rush 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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