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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +13 -0
  3. data/Gemfile.lock +86 -0
  4. data/README.rdoc +125 -0
  5. data/Rakefile +57 -0
  6. data/VERSION +1 -0
  7. data/bin/rush +13 -0
  8. data/bin/rushd +7 -0
  9. data/lib/rush.rb +91 -0
  10. data/lib/rush/access.rb +121 -0
  11. data/lib/rush/array_ext.rb +19 -0
  12. data/lib/rush/box.rb +124 -0
  13. data/lib/rush/commands.rb +76 -0
  14. data/lib/rush/config.rb +147 -0
  15. data/lib/rush/dir.rb +166 -0
  16. data/lib/rush/embeddable_shell.rb +26 -0
  17. data/lib/rush/entry.rb +229 -0
  18. data/lib/rush/exceptions.rb +32 -0
  19. data/lib/rush/file.rb +94 -0
  20. data/lib/rush/find_by.rb +39 -0
  21. data/lib/rush/fixnum_ext.rb +18 -0
  22. data/lib/rush/head_tail.rb +11 -0
  23. data/lib/rush/local.rb +377 -0
  24. data/lib/rush/process.rb +59 -0
  25. data/lib/rush/process_set.rb +62 -0
  26. data/lib/rush/remote.rb +33 -0
  27. data/lib/rush/search_results.rb +71 -0
  28. data/lib/rush/shell.rb +111 -0
  29. data/lib/rush/shell/completion.rb +110 -0
  30. data/lib/rush/string_ext.rb +16 -0
  31. data/spec/access_spec.rb +134 -0
  32. data/spec/array_ext_spec.rb +15 -0
  33. data/spec/base.rb +22 -0
  34. data/spec/box_spec.rb +76 -0
  35. data/spec/commands_spec.rb +47 -0
  36. data/spec/config_spec.rb +108 -0
  37. data/spec/dir_spec.rb +163 -0
  38. data/spec/embeddable_shell_spec.rb +17 -0
  39. data/spec/entry_spec.rb +133 -0
  40. data/spec/file_spec.rb +83 -0
  41. data/spec/find_by_spec.rb +58 -0
  42. data/spec/fixnum_ext_spec.rb +19 -0
  43. data/spec/local_spec.rb +365 -0
  44. data/spec/process_set_spec.rb +50 -0
  45. data/spec/process_spec.rb +73 -0
  46. data/spec/remote_spec.rb +140 -0
  47. data/spec/rush_spec.rb +28 -0
  48. data/spec/search_results_spec.rb +44 -0
  49. data/spec/shell_spec.rb +35 -0
  50. data/spec/ssh_tunnel_spec.rb +122 -0
  51. data/spec/string_ext_spec.rb +23 -0
  52. metadata +209 -0
@@ -0,0 +1,19 @@
1
+ # Rush mixes in Rush::Commands in order to allow operations on groups of
2
+ # Rush::Entry items. For example, dir['**/*.rb'] returns an array of files, so
3
+ # dir['**/*.rb'].destroy would destroy all the files specified.
4
+ #
5
+ # One cool tidbit: the array can contain entries anywhere, so you can create
6
+ # collections of entries from different servers and then operate across them:
7
+ #
8
+ # [ box1['/var/log/access.log'] + box2['/var/log/access.log'] ].search /#{url}/
9
+ class Array
10
+ include Rush::Commands
11
+
12
+ def entries
13
+ self
14
+ end
15
+
16
+ include Rush::FindBy
17
+
18
+ include Rush::HeadTail
19
+ end
@@ -0,0 +1,124 @@
1
+ # A rush box is a single unix machine - a server, workstation, or VPS instance.
2
+ #
3
+ # Specify a box by hostname (default = 'localhost'). If the box is remote, the
4
+ # first action performed will attempt to open an ssh tunnel. Use square
5
+ # brackets to access the filesystem, or processes to access the process list.
6
+ #
7
+ # Example:
8
+ #
9
+ # local = Rush::Box.new
10
+ # local['/etc/hosts'].contents
11
+ # local.processes
12
+ #
13
+ class Rush::Box
14
+ attr_reader :host, :local_path
15
+
16
+ # Instantiate a box. No action is taken to make a connection until you try
17
+ # to perform an action. If the box is remote, an ssh tunnel will be opened.
18
+ # Specify a username with the host if the remote ssh user is different from
19
+ # the local one (e.g. Rush::Box.new('user@host')).
20
+ def initialize(host='localhost', local_path = nil)
21
+ @host = host
22
+ @local_path = local_path
23
+ end
24
+
25
+ def to_s # :nodoc:
26
+ host
27
+ end
28
+
29
+ def inspect # :nodoc:
30
+ host
31
+ end
32
+
33
+ # Access / on the box.
34
+ def filesystem
35
+ if host == 'localhost'
36
+ Rush::Entry.factory('/', self)
37
+ else
38
+ connection.local_path
39
+ end
40
+ end
41
+
42
+ # Look up an entry on the filesystem, e.g. box['/path/to/some/file'].
43
+ # Returns a subclass of Rush::Entry - either Rush::Dir if you specifiy
44
+ # trailing slash, or Rush::File otherwise.
45
+ def [](key)
46
+ filesystem[key]
47
+ end
48
+
49
+ # Get the list of processes running on the box, not unlike "ps aux" in bash.
50
+ # Returns a Rush::ProcessSet.
51
+ def processes
52
+ Rush::ProcessSet.new(
53
+ connection.processes.map { |ps| Rush::Process.new(ps, self) }
54
+ )
55
+ end
56
+
57
+ # Guess if method missing then it's command for folder binded to that box.
58
+ #
59
+ def method_missing(meth, *args, &block)
60
+ filesystem.send(meth, *args, &block)
61
+ end
62
+
63
+ # Execute a command in the standard unix shell. Returns the contents of
64
+ # stdout if successful, or raises Rush::BashFailed with the output of stderr
65
+ # if the shell returned a non-zero value. Options:
66
+ #
67
+ # :user => unix username to become via sudo
68
+ # :env => hash of environment variables
69
+ # :background => run in the background (returns Rush::Process instead of stdout)
70
+ #
71
+ # Examples:
72
+ #
73
+ # box.bash '/etc/init.d/mysql restart', :user => 'root'
74
+ # box.bash 'rake db:migrate', :user => 'www', :env => { :RAILS_ENV => 'production' }
75
+ # box.bash 'mongrel_rails start', :background => true
76
+ # box.bash 'rake db:migrate', :user => 'www', :env => { :RAILS_ENV => 'production' }, :reset_environment => true
77
+ #
78
+ def bash(command, options = {})
79
+ cmd_with_env = command_with_environment(command, options[:env])
80
+ options[:reset_environment] ||= false
81
+
82
+ if options[:background]
83
+ pid = connection.bash(cmd_with_env, options[:user], true, options[:reset_environment])
84
+ processes.find_by_pid(pid)
85
+ else
86
+ connection.bash(cmd_with_env, options[:user], false, options[:reset_environment])
87
+ end
88
+ end
89
+
90
+ def command_with_environment(command, env) # :nodoc:
91
+ return command unless env
92
+ env.map do |key, value|
93
+ escaped = value.to_s.gsub('"', '\\"').gsub('`', '\\\`')
94
+ "export #{key}=\"#{escaped}\""
95
+ end.push(command).join("\n")
96
+ end
97
+
98
+ # Returns true if the box is responding to commands.
99
+ def alive?
100
+ connection.alive?
101
+ end
102
+
103
+ # This is called automatically the first time an action is invoked, but you
104
+ # may wish to call it manually ahead of time in order to have the tunnel
105
+ # already set up and running. You can also use this to pass a timeout option,
106
+ # either :timeout => (seconds) or :timeout => :infinite.
107
+ def establish_connection(options={})
108
+ connection.ensure_tunnel(options)
109
+ end
110
+
111
+ def connection # :nodoc:
112
+ @connection ||= make_connection
113
+ end
114
+
115
+ def make_connection # :nodoc:
116
+ host == 'localhost' ?
117
+ Rush::Connection::Local.new :
118
+ Rush::Connection::Remote.new(host, local_path)
119
+ end
120
+
121
+ def ==(other) # :nodoc:
122
+ host == other.host
123
+ end
124
+ end
@@ -0,0 +1,76 @@
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
+ entries.inject(Rush::SearchResults.new(pattern)) do |results, entry|
21
+ if !entry.dir? and matches = entry.search(pattern)
22
+ results.add(entry, matches)
23
+ end
24
+ results
25
+ end
26
+ end
27
+
28
+ # Search and replace file contents.
29
+ def replace_contents!(pattern, with_text)
30
+ entries.each do |entry|
31
+ entry.replace_contents!(pattern, with_text) unless entry.dir?
32
+ end
33
+ end
34
+
35
+ # Count the number of lines in the contained files.
36
+ def line_count
37
+ entries.inject(0) do |count, entry|
38
+ count + (entry.dir? ? 0 : entry.lines.size)
39
+ end
40
+ end
41
+
42
+ # Invoke vi on one or more files - only works locally.
43
+ def vi(*args)
44
+ if self.class == Rush::Dir
45
+ system "cd #{full_path}; vim"
46
+ else
47
+ open_with('vim', *args)
48
+ end
49
+ end
50
+ alias_method :vim, :vi
51
+
52
+ # Invoke TextMate on one or more files - only works locally.
53
+ def mate(*args)
54
+ open_with('mate', *args)
55
+ end
56
+
57
+ # Open file with xdg-open.
58
+ # Usage:
59
+ # home.locate('mai_failz').open
60
+ def open(*args)
61
+ open_with('xdg-open', *args)
62
+ end
63
+
64
+ # Open file with any application you like.
65
+ # Usage:
66
+ # home.locate('timetable').open_with :vim
67
+ def open_with(app, *args)
68
+ names = dir? ? '' : entries.map(&:to_s).join(' ')
69
+ system "cd #{dirname}; #{app.to_s} #{names} #{args.join(' ')}"
70
+ end
71
+
72
+ def output_of(app, *args)
73
+ names = entries.map(&:to_s).join(' ')
74
+ `cd #{dirname}; #{app.to_s} #{names} #{args.join(' ')}`
75
+ end
76
+ end
@@ -0,0 +1,147 @@
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
+ passwords_file.lines_or_empty.inject({}) do |result, line|
70
+ user, password = line.split(":", 2)
71
+ result.merge user => password
72
+ end
73
+ end
74
+
75
+ # Credentials is the client-side equivalent of passwords. It contains only
76
+ # one username:password combination that is transmitted to the server when
77
+ # connecting. This is also autogenerated if it does not exist.
78
+ def credentials_file
79
+ dir['credentials']
80
+ end
81
+
82
+ def credentials
83
+ credentials_file.lines.first.split(":", 2)
84
+ end
85
+
86
+ def save_credentials(user, password)
87
+ credentials_file.write("#{user}:#{password}\n")
88
+ end
89
+
90
+ def credentials_user
91
+ credentials[0]
92
+ end
93
+
94
+ def credentials_password
95
+ credentials[1]
96
+ end
97
+
98
+ def ensure_credentials_exist
99
+ generate_credentials if credentials_file.contents_or_blank == ""
100
+ end
101
+
102
+ def generate_credentials
103
+ save_credentials(generate_user, generate_password)
104
+ end
105
+
106
+ def generate_user
107
+ generate_secret(4, 8)
108
+ end
109
+
110
+ def generate_password
111
+ generate_secret(8, 15)
112
+ end
113
+
114
+ def generate_secret(min, max)
115
+ chars = self.secret_characters
116
+ len = rand(max - min + 1) + min
117
+ len.times.inject('') { |r| r += chars[rand(chars.length)] }
118
+ end
119
+
120
+ def secret_characters
121
+ [ ('a'..'z'), ('1'..'9') ].inject([]) do |chars, range|
122
+ chars += range.to_a
123
+ end
124
+ end
125
+
126
+ # ~/.rush/tunnels contains a list of previously created ssh tunnels. The
127
+ # format is host:port, where port is the local port that the tunnel is
128
+ # listening on.
129
+ def tunnels_file
130
+ dir['tunnels']
131
+ end
132
+
133
+ def tunnels
134
+ tunnels_file.lines_or_empty.inject({}) do |hash, line|
135
+ host, port = line.split(':', 2)
136
+ hash.merge host => port.to_i
137
+ end
138
+ end
139
+
140
+ def save_tunnels(hash)
141
+ string = ""
142
+ hash.each do |host, port|
143
+ string += "#{host}:#{port}\n"
144
+ end
145
+ tunnels_file.write string
146
+ end
147
+ end
@@ -0,0 +1,166 @@
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
+ alias_method :dirname, :full_path
23
+
24
+ # Entries contained within this dir - not recursive.
25
+ def contents
26
+ find_by_glob('*')
27
+ end
28
+
29
+ # Files contained in this dir only.
30
+ def files
31
+ contents.reject(&:dir?)
32
+ end
33
+
34
+ # Other dirs contained in this dir only.
35
+ def dirs
36
+ contents.select(&:dir?)
37
+ end
38
+
39
+ # Access subentries with square brackets, e.g. dir['subdir/file']
40
+ def [](key)
41
+ key = key.to_s
42
+ case
43
+ when key == '**' then files_flattened
44
+ when key.match(/\*/) then find_by_glob(key)
45
+ else find_by_name(key)
46
+ end
47
+ end
48
+ # Slashes work as well, e.g. dir/'subdir/file'
49
+ alias_method :/, :[]
50
+
51
+ def locate(path)
52
+ located = bash("locate #{path}").split("\n").
53
+ map { |x| x.dir? ? Rush::Dir.new(x) : Rush::File.new(x) }
54
+ located.size == 1 ? located.first : located
55
+ end
56
+
57
+ def locate_dir(path)
58
+ located = bash("locate -r #{path}$").split("\n").
59
+ map { |x| Dir.new x }
60
+ located.size == 1 ? located.first : located
61
+ end
62
+
63
+ def find_by_name(name) # :nodoc:
64
+ Rush::Entry.factory("#{full_path}/#{name}", box)
65
+ end
66
+
67
+ def find_by_glob(glob) # :nodoc:
68
+ connection.index(full_path, glob).map do |fname|
69
+ Rush::Entry.factory("#{full_path}/#{fname}", box)
70
+ end
71
+ end
72
+
73
+ # A list of all the recursively contained entries in flat form.
74
+ def entries_tree
75
+ find_by_glob('**/*')
76
+ end
77
+
78
+ # Recursively contained files.
79
+ def files_flattened
80
+ entries_tree.reject(&:dir?)
81
+ end
82
+
83
+ # Recursively contained dirs.
84
+ def dirs_flattened
85
+ entries_tree.select(&:dir?)
86
+ end
87
+
88
+ # Given a list of flat filenames, product a list of entries under this dir.
89
+ # Mostly for internal use.
90
+ def make_entries(filenames)
91
+ Array(filenames).
92
+ map { |fname| Rush::Entry.factory("#{full_path}/#{fname}") }
93
+ end
94
+
95
+ # Create a blank file within this dir.
96
+ def create_file(name)
97
+ file = self[name].create
98
+ file.write('')
99
+ file
100
+ end
101
+
102
+ # Create an empty subdir within this dir.
103
+ def create_dir(name)
104
+ name += '/' unless name[-1] == '/'
105
+ self[name].create
106
+ end
107
+
108
+ # Create an instantiated but not yet filesystem-created dir.
109
+ def create
110
+ connection.create_dir(full_path)
111
+ self
112
+ end
113
+
114
+ # Get the total disk usage of the dir and all its contents.
115
+ def size
116
+ connection.size(full_path)
117
+ end
118
+
119
+ # Contained dirs that are not hidden.
120
+ def nonhidden_dirs
121
+ dirs.reject(&:hidden?)
122
+ end
123
+
124
+ # Contained files that are not hidden.
125
+ def nonhidden_files
126
+ files.reject(&:hidden?)
127
+ end
128
+
129
+ # Run a bash command starting in this directory. Options are the same as Rush::Box#bash.
130
+ def bash(command, options={})
131
+ box.bash "cd #{quoted_path} && #{command}", options
132
+ end
133
+
134
+ # Destroy all of the contents of the directory, leaving it fresh and clean.
135
+ def purge
136
+ connection.purge full_path
137
+ end
138
+
139
+ # Text output of dir listing, equivalent to the regular unix shell's ls command.
140
+ def ls
141
+ out = [ "#{self}" ]
142
+ nonhidden_dirs.each do |dir|
143
+ out << " #{dir.name}/"
144
+ end
145
+ nonhidden_files.each do |file|
146
+ out << " #{file.name}"
147
+ end
148
+ out.join("\n")
149
+ end
150
+
151
+ # Run rake within this dir.
152
+ def rake(*args)
153
+ bash "rake #{args.join(' ')}"
154
+ end
155
+
156
+ # Run git within this dir.
157
+ def git(*args)
158
+ bash "git #{args.join(' ')}"
159
+ end
160
+
161
+ include Rush::Commands
162
+
163
+ def entries
164
+ contents
165
+ end
166
+ end