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