rush 0.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.
@@ -0,0 +1,86 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc "Run all specs"
5
+ Spec::Rake::SpecTask.new('spec') do |t|
6
+ t.spec_files = FileList['spec/*_spec.rb']
7
+ end
8
+
9
+ desc "Print specdocs"
10
+ Spec::Rake::SpecTask.new(:doc) do |t|
11
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
12
+ t.spec_files = FileList['spec/*_spec.rb']
13
+ end
14
+
15
+ desc "Run all examples with RCov"
16
+ Spec::Rake::SpecTask.new('rcov') do |t|
17
+ t.spec_files = FileList['spec/*_spec.rb']
18
+ t.rcov = true
19
+ t.rcov_opts = ['--exclude', 'examples']
20
+ end
21
+
22
+ task :default => :spec
23
+
24
+ ######################################################
25
+
26
+ require 'rake'
27
+ require 'rake/testtask'
28
+ require 'rake/clean'
29
+ require 'rake/gempackagetask'
30
+ require 'rake/rdoctask'
31
+ require 'fileutils'
32
+ include FileUtils
33
+
34
+ version = "0.1"
35
+ name = "rush"
36
+
37
+ spec = Gem::Specification.new do |s|
38
+ s.name = name
39
+ s.version = version
40
+ s.summary = "A Ruby replacement for bash+ssh."
41
+ s.description = "A Ruby replacement for bash+ssh, providing both an interactive shell and a library. Manage both local and remote unix systems from a single client."
42
+ s.author = "Adam Wiggins"
43
+ s.email = "adam@heroku.com"
44
+ s.homepage = "http://rush.heroku.com/"
45
+ s.executables = [ "rush", "rushd" ]
46
+ s.rubyforge_project = "ruby-shell"
47
+
48
+ s.platform = Gem::Platform::RUBY
49
+ s.has_rdoc = true
50
+
51
+ s.files = %w(Rakefile) + Dir.glob("{bin,lib,spec}/**/*")
52
+
53
+ s.require_path = "lib"
54
+ s.bindir = "bin"
55
+ end
56
+
57
+ Rake::GemPackageTask.new(spec) do |p|
58
+ p.need_tar = true if RUBY_PLATFORM !~ /mswin/
59
+ end
60
+
61
+ task :install => [ :package ] do
62
+ sh %{sudo gem install pkg/#{name}-#{version}.gem}
63
+ end
64
+
65
+ task :uninstall => [ :clean ] do
66
+ sh %{sudo gem uninstall #{name}}
67
+ end
68
+
69
+ Rake::TestTask.new do |t|
70
+ t.libs << "spec"
71
+ t.test_files = FileList['spec/*_spec.rb']
72
+ t.verbose = true
73
+ end
74
+
75
+ Rake::RDocTask.new do |t|
76
+ t.rdoc_dir = 'doc'
77
+ t.title = "rush, a Ruby replacement for bash+ssh"
78
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
79
+ t.options << '--charset' << 'utf-8'
80
+ t.rdoc_files.include('README')
81
+ t.rdoc_files.include('lib/rush.rb')
82
+ t.rdoc_files.include('lib/rush/*.rb')
83
+ end
84
+
85
+ CLEAN.include [ 'pkg', '*.gem', '.config' ]
86
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/rush'
4
+ require 'shell'
5
+ Rush::Shell.new.run
6
+
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/rush'
4
+ require 'server'
5
+ RushServer.new.run
6
+
@@ -0,0 +1,20 @@
1
+ module Rush; end
2
+ module Rush::Connection; end
3
+
4
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/rush')
5
+
6
+ require 'config'
7
+ require 'commands'
8
+ require 'entry'
9
+ require 'file'
10
+ require 'dir'
11
+ require 'search_results'
12
+ require 'head_tail'
13
+ require 'string_ext'
14
+ require 'fixnum_ext'
15
+ require 'array_ext'
16
+ require 'process'
17
+ require 'local'
18
+ require 'remote'
19
+ require 'ssh_tunnel'
20
+ require 'box'
@@ -0,0 +1,17 @@
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::HeadTail
17
+ end
@@ -0,0 +1,63 @@
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('localhost')
10
+ # local['/etc/hosts'].contents
11
+ # local.processes
12
+ #
13
+ class Rush::Box
14
+ attr_reader :host
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')
21
+ @host = host
22
+ end
23
+
24
+ def to_s # :nodoc:
25
+ host
26
+ end
27
+
28
+ def inspect # :nodoc:
29
+ host
30
+ end
31
+
32
+ # Access / on the box.
33
+ def filesystem
34
+ Rush::Entry.factory('/', self)
35
+ end
36
+
37
+ # Look up an entry on the filesystem, e.g. box['/path/to/some/file'].
38
+ # Returns a subclass of Rush::Entry - either Rush::Dir if you specifiy
39
+ # trailing slash, or Rush::File otherwise.
40
+ def [](key)
41
+ filesystem[key]
42
+ end
43
+
44
+ # Get the list of processes currently running on the box. Returns an array
45
+ # of Rush::Process.
46
+ def processes
47
+ connection.processes.map do |ps|
48
+ Rush::Process.new(ps, self)
49
+ end
50
+ end
51
+
52
+ def connection # :nodoc:
53
+ @connection ||= make_connection
54
+ end
55
+
56
+ def make_connection # :nodoc:
57
+ host == 'localhost' ? Rush::Connection::Local.new : Rush::Connection::Remote.new(host)
58
+ end
59
+
60
+ def ==(other) # :nodoc:
61
+ host == other.host
62
+ end
63
+ end
@@ -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.full_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.full_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
@@ -0,0 +1,148 @@
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
+
50
+ def find_by_name(name) # :nodoc:
51
+ Rush::Entry.factory("#{full_path}/#{name}", box)
52
+ end
53
+
54
+ def find_by_glob(glob) # :nodoc:
55
+ connection.index(full_path, glob).map do |fname|
56
+ Rush::Entry.factory("#{full_path}/#{fname}", box)
57
+ end
58
+ end
59
+
60
+ # A list of all the recursively contained entries in flat form.
61
+ def entries_tree
62
+ find_by_glob('**/*')
63
+ end
64
+
65
+ # Recursively contained files.
66
+ def files_flattened
67
+ entries_tree.select { |e| !e.dir? }
68
+ end
69
+
70
+ # Recursively contained dirs.
71
+ def dirs_flattened
72
+ entries_tree.select { |e| e.dir? }
73
+ end
74
+
75
+ # Given a list of flat filenames, product a list of entries under this dir.
76
+ # Mostly for internal use.
77
+ def make_entries(filenames)
78
+ filenames.map do |fname|
79
+ Rush::Entry.factory("#{full_path}/#{fname}")
80
+ end
81
+ end
82
+
83
+ # Create a blank file within this dir.
84
+ def create_file(name)
85
+ file = self[name].create
86
+ file.write('')
87
+ file
88
+ end
89
+
90
+ # Create an empty subdir within this dir.
91
+ def create_dir(name)
92
+ name += '/' unless name.tail(1) == '/'
93
+ self[name].create
94
+ end
95
+
96
+ # Create an instantiated but not yet filesystem-created dir.
97
+ def create
98
+ connection.create_dir(full_path)
99
+ self
100
+ end
101
+
102
+ # Get the total disk usage of the dir and all its contents.
103
+ def size
104
+ connection.size(full_path)
105
+ end
106
+
107
+ # Contained dirs that are not hidden.
108
+ def nonhidden_dirs
109
+ dirs.select do |dir|
110
+ !dir.hidden?
111
+ end
112
+ end
113
+
114
+ # Contained files that are not hidden.
115
+ def nonhidden_files
116
+ files.select do |file|
117
+ !file.hidden?
118
+ end
119
+ end
120
+
121
+ # Text output of dir listing, equivalent to the regular unix shell's ls command.
122
+ def ls
123
+ out = [ "#{self}" ]
124
+ nonhidden_dirs.each do |dir|
125
+ out << " #{dir.name}+"
126
+ end
127
+ nonhidden_files.each do |file|
128
+ out << " #{file.name}"
129
+ end
130
+ out.join("\n")
131
+ end
132
+
133
+ # Run rake within this dir.
134
+ def rake(*args)
135
+ system "cd #{full_path}; rake #{args.join(' ')}"
136
+ end
137
+
138
+ # Run git within this dir.
139
+ def git(*args)
140
+ system "cd #{full_path}; git #{args.join(' ')}"
141
+ end
142
+
143
+ include Rush::Commands
144
+
145
+ def entries
146
+ contents
147
+ end
148
+ end