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
data/README.rdoc ADDED
@@ -0,0 +1,89 @@
1
+ = rush -- manage your unix systems with pure Ruby
2
+
3
+ rush is a unix integration library and an interactive shell which uses pure Ruby syntax. Walk directory trees; create, copy, search, and destroy files; find and kill processes - everything you'd normally do with shell commands, now in the strict and elegant world of Ruby.
4
+
5
+ == Usage
6
+
7
+ Count the number of classes in your project using bash:
8
+
9
+ find myproj -name \*.rb | xargs grep '^\s*class' | wc -l
10
+
11
+ In rush, this is:
12
+
13
+ myproj['**/*.rb'].search(/^\s*class/).lines.size
14
+
15
+ Pesky stray mongrels? In bash:
16
+
17
+ kill `ps aux | grep mongrel_rails | grep -v grep | cut -c 10-20`
18
+
19
+ In rush:
20
+
21
+ processes.filter(:cmdline => /mongrel_rails/).kill
22
+
23
+ == As a library
24
+
25
+ require 'rubygems'
26
+ require 'rush'
27
+
28
+ file = Rush['/tmp/myfile']
29
+ file.write "hello"
30
+ puts file.contents
31
+ file.destroy
32
+
33
+ puts Rush.my_process.pid
34
+ puts Rush.processes.size
35
+ puts Rush.bash("echo SHELL COMMAND | tr A-Z a-z")
36
+ puts Rush.launch_dir['*.rb'].search(/Rush/).entries.inspect
37
+
38
+ == Invoking the shell
39
+
40
+ Run the "rush" binary to enter the interactive shell.
41
+
42
+ == Remote access and clustering
43
+
44
+ rush can control any number of remote machines from a single location. Copy files or directories between servers as seamlessly as if it was all local.
45
+
46
+ Example of remote access:
47
+
48
+ local = Rush::Box.new('localhost')
49
+ remote = Rush::Box.new('my.remote.server.com')
50
+ local_dir = local['/Users/adam/myproj/']
51
+ remote_dir = remote['/home/myproj/app/']
52
+
53
+ local_dir.copy_to remote_dir
54
+ remote_dir['**/.svn/'].each { |d| d.destroy }
55
+
56
+ Clustering:
57
+
58
+ local_dir = Rush::Box.new('localhost')['/Users/adam/server_logs/'].create
59
+ servers = %w(www1 www2 www3).map { |n| Rush::Box.new(n) }
60
+ servers.each { |s| s['/var/log/nginx/access.log'].copy_to local_dir["#{s.host}_access.log"] }
61
+
62
+ == Reference
63
+
64
+ For more details on syntax and commands, see:
65
+
66
+ * Rush
67
+ * Rush::Entry
68
+ * Rush::File
69
+ * Rush::Dir
70
+ * Rush::Commands
71
+ * Rush::Box
72
+ * Rush::Process
73
+
74
+ == Meta
75
+
76
+ Created by Adam Wiggins
77
+
78
+ Patches contributed by Chihiro Ito, Gabriel Ware, Michael Schutte, Ricardo Chimal Jr., and Nicholas Schlueter, Pedro Belo, and Martin Kuehl
79
+
80
+ Forked by Aleksey Palazhchenko.
81
+
82
+ Logo by James Lindenbaum
83
+
84
+ Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
85
+
86
+ http://rush.heroku.com
87
+
88
+ http://groups.google.com/group/ruby-shell
89
+
data/Rakefile ADDED
@@ -0,0 +1,59 @@
1
+ require 'rake'
2
+
3
+ require 'jeweler'
4
+
5
+ Jeweler::Tasks.new do |s|
6
+ s.name = "aleksi-rush"
7
+ s.summary = "A Ruby replacement for bash+ssh."
8
+ 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."
9
+ s.author = "Adam Wiggins, Aleksey Palazhchenko"
10
+ s.email = "aleksey.palazhchenko@gmail.com"
11
+ s.homepage = "http://github.com/AlekSi/rush"
12
+ s.executables = [ "rush", "rushd" ]
13
+ s.has_rdoc = true
14
+
15
+ s.add_dependency 'session'
16
+
17
+ s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
18
+ end
19
+
20
+ Jeweler::GemcutterTasks.new
21
+
22
+ ######################################################
23
+
24
+ require 'spec/rake/spectask'
25
+
26
+ desc "Run all specs"
27
+ Spec::Rake::SpecTask.new('spec') do |t|
28
+ t.spec_files = FileList['spec/*_spec.rb']
29
+ end
30
+
31
+ desc "Print specdocs"
32
+ Spec::Rake::SpecTask.new(:doc) do |t|
33
+ t.spec_opts = ["--format", "specdoc", "--dry-run"]
34
+ t.spec_files = FileList['spec/*_spec.rb']
35
+ end
36
+
37
+ desc "Run all examples with RCov"
38
+ Spec::Rake::SpecTask.new('rcov') do |t|
39
+ t.spec_files = FileList['spec/*_spec.rb']
40
+ t.rcov = true
41
+ t.rcov_opts = ['--exclude', 'examples']
42
+ end
43
+
44
+ task :default => :spec
45
+
46
+ ######################################################
47
+
48
+ require 'rake/rdoctask'
49
+
50
+ Rake::RDocTask.new do |t|
51
+ t.rdoc_dir = 'rdoc'
52
+ t.title = "rush, a Ruby replacement for bash+ssh"
53
+ t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
54
+ t.options << '--charset' << 'utf-8'
55
+ t.rdoc_files.include('README.rdoc')
56
+ t.rdoc_files.include('lib/rush.rb')
57
+ t.rdoc_files.include('lib/rush/*.rb')
58
+ end
59
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.6
data/bin/rush ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/rush'
4
+ require File.dirname(__FILE__) + '/../lib/rush/shell'
5
+
6
+ shell = Rush::Shell.new
7
+
8
+ if ARGV.size > 0
9
+ shell.execute ARGV.join(' ')
10
+ else
11
+ shell.run
12
+ end
13
+
data/bin/rushd ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.dirname(__FILE__) + '/../lib/rush'
4
+ require File.dirname(__FILE__) + '/../lib/rush/server'
5
+
6
+ RushServer.new.run
7
+
data/lib/rush.rb ADDED
@@ -0,0 +1,87 @@
1
+ require 'rubygems'
2
+
3
+ # The top-level Rush module has some convenience methods for accessing the
4
+ # local box.
5
+ module Rush
6
+ # Access the root filesystem of the local box. Example:
7
+ #
8
+ # Rush['/etc/hosts'].contents
9
+ #
10
+ def self.[](key)
11
+ box[key]
12
+ end
13
+
14
+ # Create a dir object from the path of a provided file. Example:
15
+ #
16
+ # Rush.dir(__FILE__).files
17
+ #
18
+ def self.dir(filename)
19
+ box[::File.expand_path(::File.dirname(filename)) + '/']
20
+ end
21
+
22
+ # Create a dir object based on the shell's current working directory at the
23
+ # time the program was run. Example:
24
+ #
25
+ # Rush.launch_dir.files
26
+ #
27
+ def self.launch_dir
28
+ box[::Dir.pwd + '/']
29
+ end
30
+
31
+ # Run a bash command in the root of the local machine. Equivalent to
32
+ # Rush::Box.new.bash.
33
+ def self.bash(command, options={})
34
+ box.bash(command, options)
35
+ end
36
+
37
+ # Pull the process list for the local machine. Example:
38
+ #
39
+ # Rush.processes.filter(:cmdline => /ruby/)
40
+ #
41
+ def self.processes
42
+ box.processes
43
+ end
44
+
45
+ # Get the process object for this program's PID. Example:
46
+ #
47
+ # puts "I'm using #{Rush.my_process.mem} blocks of memory"
48
+ #
49
+ def self.my_process
50
+ box.processes.filter(:pid => ::Process.pid).first
51
+ end
52
+
53
+ # Create a box object for localhost.
54
+ def self.box
55
+ @@box = Rush::Box.new
56
+ end
57
+
58
+ # Quote a path for use in backticks, say.
59
+ def self.quote(path)
60
+ path.gsub(/(?=[^a-zA-Z0-9_.\/\-\x7F-\xFF\n])/n, '\\').gsub(/\n/, "'\n'").sub(/^$/, "''")
61
+ end
62
+ end
63
+
64
+ module Rush::Connection; end
65
+
66
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
67
+
68
+ require 'rush/exceptions'
69
+ require 'rush/config'
70
+ require 'rush/commands'
71
+ require 'rush/access'
72
+ require 'rush/entry'
73
+ require 'rush/file'
74
+ require 'rush/dir'
75
+ require 'rush/search_results'
76
+ require 'rush/head_tail'
77
+ require 'rush/find_by'
78
+ require 'rush/string_ext'
79
+ require 'rush/fixnum_ext'
80
+ require 'rush/array_ext'
81
+ require 'rush/process'
82
+ require 'rush/process_set'
83
+ require 'rush/local'
84
+ require 'rush/remote'
85
+ require 'rush/ssh_tunnel'
86
+ require 'rush/box'
87
+ require 'rush/embeddable_shell'
@@ -0,0 +1,130 @@
1
+ # A class to hold permissions (read, write, execute) for files and dirs.
2
+ # See Rush::Entry#access= for information on the public-facing interface.
3
+ class Rush::Access
4
+ attr_accessor :user_can_read, :user_can_write, :user_can_execute
5
+ attr_accessor :group_can_read, :group_can_write, :group_can_execute
6
+ attr_accessor :other_can_read, :other_can_write, :other_can_execute
7
+
8
+ def self.roles
9
+ %w(user group other)
10
+ end
11
+
12
+ def self.permissions
13
+ %w(read write execute)
14
+ end
15
+
16
+ def parse(options)
17
+ options.each do |key, value|
18
+ next unless m = key.to_s.match(/(.*)_can$/)
19
+ key = m[1].to_sym
20
+ roles = extract_list('role', key, self.class.roles)
21
+ perms = extract_list('permission', value, self.class.permissions)
22
+ set_matrix(perms, roles)
23
+ end
24
+ self
25
+ end
26
+
27
+ def self.parse(options)
28
+ new.parse(options)
29
+ end
30
+
31
+ def apply(full_path)
32
+ FileUtils.chmod(octal_permissions, full_path)
33
+ rescue Errno::ENOENT
34
+ raise Rush::DoesNotExist, full_path
35
+ end
36
+
37
+ def to_hash
38
+ hash = {}
39
+ self.class.roles.each do |role|
40
+ self.class.permissions.each do |perm|
41
+ key = "#{role}_can_#{perm}".to_sym
42
+ hash[key] = send(key) ? 1 : 0
43
+ end
44
+ end
45
+ hash
46
+ end
47
+
48
+ def display_hash
49
+ hash = {}
50
+ to_hash.each do |key, value|
51
+ hash[key] = true if value == 1
52
+ end
53
+ hash
54
+ end
55
+
56
+ def from_hash(hash)
57
+ self.class.roles.each do |role|
58
+ self.class.permissions.each do |perm|
59
+ key = "#{role}_can_#{perm}"
60
+ send("#{key}=".to_sym, hash[key.to_sym].to_i == 1 ? true : false)
61
+ end
62
+ end
63
+ self
64
+ end
65
+
66
+ def self.from_hash(hash)
67
+ new.from_hash(hash)
68
+ end
69
+
70
+ def octal_permissions
71
+ perms = [ 0, 0, 0 ]
72
+ perms[0] += 4 if user_can_read
73
+ perms[0] += 2 if user_can_write
74
+ perms[0] += 1 if user_can_execute
75
+
76
+ perms[1] += 4 if group_can_read
77
+ perms[1] += 2 if group_can_write
78
+ perms[1] += 1 if group_can_execute
79
+
80
+ perms[2] += 4 if other_can_read
81
+ perms[2] += 2 if other_can_write
82
+ perms[2] += 1 if other_can_execute
83
+
84
+ eval("0" + perms.join)
85
+ end
86
+
87
+ def from_octal(mode)
88
+ perms = octal_integer_array(mode)
89
+
90
+ self.user_can_read = (perms[0] & 4) > 0 ? true : false
91
+ self.user_can_write = (perms[0] & 2) > 0 ? true : false
92
+ self.user_can_execute = (perms[0] & 1) > 0 ? true : false
93
+
94
+ self.group_can_read = (perms[1] & 4) > 0 ? true : false
95
+ self.group_can_write = (perms[1] & 2) > 0 ? true : false
96
+ self.group_can_execute = (perms[1] & 1) > 0 ? true : false
97
+
98
+ self.other_can_read = (perms[2] & 4) > 0 ? true : false
99
+ self.other_can_write = (perms[2] & 2) > 0 ? true : false
100
+ self.other_can_execute = (perms[2] & 1) > 0 ? true : false
101
+
102
+ self
103
+ end
104
+
105
+ def octal_integer_array(mode)
106
+ mode %= 01000 # filter out everything but the bottom three digits
107
+ mode = sprintf("%o", mode) # convert to string
108
+ mode.split("").map { |p| p.to_i } # and finally, array of integers
109
+ end
110
+
111
+ def set_matrix(perms, roles)
112
+ perms.each do |perm|
113
+ roles.each do |role|
114
+ meth = "#{role}_can_#{perm}=".to_sym
115
+ send(meth, true)
116
+ end
117
+ end
118
+ end
119
+
120
+ def extract_list(type, value, choices)
121
+ list = parts_from(value)
122
+ list.each do |value|
123
+ raise(Rush::BadAccessSpecifier, "Unrecognized #{type}: #{value}") unless choices.include? value
124
+ end
125
+ end
126
+
127
+ def parts_from(value)
128
+ value.to_s.split('_').reject { |r| r == 'and' }
129
+ end
130
+ end
@@ -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
data/lib/rush/box.rb ADDED
@@ -0,0 +1,115 @@
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
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 running on the box, not unlike "ps aux" in bash.
45
+ # Returns a Rush::ProcessSet.
46
+ def processes
47
+ Rush::ProcessSet.new(
48
+ connection.processes.map do |ps|
49
+ Rush::Process.new(ps, self)
50
+ end
51
+ )
52
+ end
53
+
54
+ # Execute a command in the standard unix shell. Returns the contents of
55
+ # stdout if successful, or raises Rush::BashFailed with the output of stderr
56
+ # if the shell returned a non-zero value. Options:
57
+ #
58
+ # :user => unix username to become via sudo
59
+ # :env => hash of environment variables
60
+ # :background => run in the background (returns Rush::Process instead of stdout)
61
+ #
62
+ # Examples:
63
+ #
64
+ # box.bash '/etc/init.d/mysql restart', :user => 'root'
65
+ # box.bash 'rake db:migrate', :user => 'www', :env => { :RAILS_ENV => 'production' }
66
+ # box.bash 'mongrel_rails start', :background => true
67
+ # box.bash 'rake db:migrate', :user => 'www', :env => { :RAILS_ENV => 'production' }, :reset_environment => true
68
+ #
69
+ def bash(command, options={})
70
+ cmd_with_env = command_with_environment(command, options[:env])
71
+ options[:reset_environment] ||= false
72
+
73
+ if options[:background]
74
+ pid = connection.bash(cmd_with_env, options[:user], true, options[:reset_environment])
75
+ processes.find_by_pid(pid)
76
+ else
77
+ connection.bash(cmd_with_env, options[:user], false, options[:reset_environment])
78
+ end
79
+ end
80
+
81
+ def command_with_environment(command, env) # :nodoc:
82
+ return command unless env
83
+
84
+ vars = env.sort_by{ |key, value| key.to_s }.map do |key, value|
85
+ escaped = value.to_s.gsub('"', '\\"').gsub('`', '\\\`')
86
+ "export #{key}=\"#{escaped}\""
87
+ end
88
+ vars.push(command).join("\n")
89
+ end
90
+
91
+ # Returns true if the box is responding to commands.
92
+ def alive?
93
+ connection.alive?
94
+ end
95
+
96
+ # This is called automatically the first time an action is invoked, but you
97
+ # may wish to call it manually ahead of time in order to have the tunnel
98
+ # already set up and running. You can also use this to pass a timeout option,
99
+ # either :timeout => (seconds) or :timeout => :infinite.
100
+ def establish_connection(options={})
101
+ connection.ensure_tunnel(options)
102
+ end
103
+
104
+ def connection # :nodoc:
105
+ @connection ||= make_connection
106
+ end
107
+
108
+ def make_connection # :nodoc:
109
+ host == 'localhost' ? Rush::Connection::Local.new : Rush::Connection::Remote.new(host)
110
+ end
111
+
112
+ def ==(other) # :nodoc:
113
+ host == other.host
114
+ end
115
+ end