shellter 0.9.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.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ .rvmrc
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in shellter.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Andrew Eberbach
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # Shellter
2
+
3
+ Do YOU like to fork(2)? Do YOU like cocaine(https://github.com/thoughtbot/cocaine)? Of course, we all do. The problem is that I needed a ever-rounder wheel than what I've found. The Escape gem was a nice place to find shell escaping... but it didn't do interpolation or actually execute anything. The Cocaine gem was a nice place to find interpolation, but its escaping was a bit too simple for my tastes and it didn't manage STDERR, STDOUT in a way that let me inspect them easily afterward. I've also always wanted to be able to start a process in the background without having to worry about what I need to close and open and how many times I need to fork (but if you want to know everything about it, I'd suggest you read http://workingwithunixprocesses.com/).
4
+
5
+ So in the grand spirit of the GitHubs here's a gem that does all of the above. The idea is that your result is going to be more than just an exit status. Here's what you'll get:
6
+
7
+ * The fully interpolated command that was last run
8
+ * The exit status
9
+ * The entire STDERR and STDOUT
10
+ * The PID
11
+
12
+ Yes, of course I'm using POpen4 under the covers. The interpolation is taken from Cocaine and modified to use Escape to do the shell escaping.
13
+
14
+ The point is that it's supposed to be an easy-to-remember, easy-to-use library that takes care of everything else.
15
+
16
+ ## Backgrounding
17
+
18
+ Sure there's a ton of gems that let you manage background processes and the like. Daemons (http://daemons.rubyforge.org/) is an obvious example. Sometimes, though, you just want to "fire and forget." Shellter provides a SIMPLE way of demonizing a command whilst returning you its PID so that you can do with it what you like.
19
+
20
+ ## Installation
21
+
22
+ Add this line to your application's Gemfile:
23
+
24
+ gem 'shellter'
25
+
26
+ And then execute:
27
+
28
+ $ bundle
29
+
30
+ Or install it yourself as:
31
+
32
+ $ gem install shellter
33
+
34
+ ## Usage
35
+
36
+ The most basic usage is like this:
37
+
38
+ result = Shellter.run("ls")
39
+
40
+ That's it. This returns a Shellter::Command which has the following variables:
41
+
42
+ result.pid
43
+ result.stdout
44
+ result.stderr
45
+ result.last_command # the fully-interpolated command that was run
46
+
47
+ Want to pass arguments that you KNOW are safe?
48
+
49
+ Shellter.run("ls", "-l", "*.rb") # ls -l *.rb
50
+
51
+ Want to pass arguments that might have spaces in them?
52
+
53
+ myvar = "'\"\'some ../../weird#""'`#{}` filena_?me"
54
+ Shellter.run("ls", "-l", ":weird", :weird => myvar) # ls -l \''"'\''some ../../weird#'\''`` filena_?me'
55
+
56
+ hateful = "'$(rm -rf *.foo)"
57
+ Shellter.run("ls", "-l", ":weird", :weird => hateful) # ls -l \''$(rm -rf *.foo)'
58
+
59
+ Want to raise an exception if the command doesn't return 0?
60
+
61
+ Shellter.run!("ls", "*.missing") # RuntimeError: Execution failed, with exit code 256
62
+
63
+ Want to find out if the command ran ok?
64
+
65
+ Shellter.run("ls", "*.missing").success? # false
66
+
67
+ Want to provide a list of exit codes that you're ok with?
68
+
69
+ Shellter.run!("ls", "*.missing", :expected_outcodes => [0, 256]).success? # true
70
+
71
+ Want to start that command in the background?
72
+
73
+ Shellter.run("ls", :background => true)
74
+
75
+ *NOTE:* When backgrounded, the command `fork`s twice in order to fully detach from the session. This, of course, means that the stderr, and stdout will be empty. The pid, however, will be of the last `fork` before `exec`-ing.
76
+
77
+ *NOTE:* Before backgrounding we check to see if `fork` is actually available and throw an Exception if it isn't.
78
+
79
+ Also included is an implementation of the UNIX `which` command:
80
+
81
+ Shellter.which("ls") # /bin/ls
82
+
83
+ ## Contributing
84
+
85
+ 1. Fork it
86
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
87
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
88
+ 4. Push to the branch (`git push origin my-new-feature`)
89
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
@@ -0,0 +1,39 @@
1
+ module Shellter
2
+ module ClassMethods
3
+ def run(command, *arguments)
4
+ Command.new(command, *arguments).tap do |command|
5
+ options = arguments.extract_options!
6
+ command.run(options)
7
+ end
8
+ end
9
+
10
+ def run!(command, *arguments)
11
+ run(command, *arguments).tap do |result|
12
+ raise "Execution failed, with exit code #{result.exit_code}" unless result.success?
13
+ end
14
+ end
15
+
16
+ def which(command, paths = nil)
17
+ paths ||= ENV["PATH"].split(File::PATH_SEPARATOR)
18
+ extensions = ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']
19
+
20
+ paths.each do |path|
21
+ extensions.each do |extension|
22
+ path_with_extension(path, command, extension).tap do |path_to_command|
23
+ if File.exists?(path_to_command) && File.executable?(path_to_command)
24
+ return path_to_command
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ nil
31
+ end
32
+
33
+ private
34
+
35
+ def path_with_extension(path, command, extension)
36
+ File.join(File.expand_path(path), "#{command}#{extension}")
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,111 @@
1
+ module Shellter
2
+ class Command
3
+
4
+ attr_reader :pid
5
+ attr_reader :stdout
6
+ attr_reader :stderr
7
+ attr_reader :last_command
8
+
9
+ def initialize(command, *arguments)
10
+ options = arguments.extract_options!
11
+
12
+ @expected_outcodes = options.delete(:expected_outcodes)
13
+ @expected_outcodes ||= [0]
14
+
15
+ @command = command
16
+ @arguments = arguments
17
+ end
18
+
19
+ def run(options = {})
20
+ with_env(options) do
21
+ with_runner(options) do |runner|
22
+ with_interpolated_command(options) do |command|
23
+ @status = runner.run(command) do |stdout, stderr, stdin, pid|
24
+ stdin.close
25
+ @stdout = stdout.read
26
+ @stderr = stderr.read
27
+ @pid = pid
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+
34
+ def exit_code
35
+ @status.to_i
36
+ end
37
+
38
+ def success?
39
+ @expected_outcodes.include?(exit_code)
40
+ end
41
+
42
+ private
43
+
44
+ def interpolate(pattern, vars)
45
+ # interpolates :variables and :{variables}
46
+ pattern.gsub(%r#:(?:\w+|\{\w+\})#) do |match|
47
+ key = match[1..-1]
48
+ key = key[1..-2] if key[0,1] == '{'
49
+ if invalid_variables.include?(key)
50
+ raise InterpolationError,
51
+ "Interpolation of #{key} isn't allowed."
52
+ end
53
+ interpolation(vars, key) || match
54
+ end
55
+ end
56
+
57
+ def invalid_variables
58
+ %w(expected_outcodes swallow_stderr logger)
59
+ end
60
+
61
+ def interpolation(vars, key)
62
+ if vars.key?(key.to_sym)
63
+ Escape.shell_single_word(vars[key.to_sym])
64
+ end
65
+ end
66
+
67
+ def with_env(options = {}, &block)
68
+ begin
69
+ saved_env = ENV.to_hash
70
+ # Bundler.with_clean_env(&block)
71
+ # command = %Q{. #{vars_file_path} && #{script}}
72
+ # ENV.update(self.class.environment)
73
+ yield
74
+ ensure
75
+ ENV.update(saved_env)
76
+ end
77
+ end
78
+
79
+ def with_interpolated_command(options, &block)
80
+ [].tap do |command_line|
81
+ command_line << Escape.shell_single_word(@command)
82
+ command_line += interpolated_arguments(options)
83
+
84
+ command_line.join(" ").tap do |command|
85
+ @last_command = command
86
+ yield command
87
+ end
88
+ end
89
+ end
90
+
91
+ def interpolated_arguments(vars)
92
+ @arguments.map do |argument|
93
+ interpolate(argument, vars)
94
+ end
95
+ end
96
+
97
+ def with_runner(options = {}, &block)
98
+ if options[:background]
99
+ yield Runners::Background.new(options)
100
+ elsif windows?
101
+ yield Runners::Windows.new(options)
102
+ else
103
+ yield Runners::Base.new(options)
104
+ end
105
+ end
106
+
107
+ def windows?
108
+ !!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/)
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,36 @@
1
+ begin
2
+ require 'active_support/core_ext/array/extract_options'
3
+ rescue LoadError
4
+
5
+ class Hash
6
+ # By default, only instances of Hash itself are extractable.
7
+ # Subclasses of Hash may implement this method and return
8
+ # true to declare themselves as extractable. If a Hash
9
+ # is extractable, Array#extract_options! pops it from
10
+ # the Array when it is the last element of the Array.
11
+ def extractable_options?
12
+ instance_of?(Hash)
13
+ end
14
+ end
15
+
16
+ class Array
17
+ # Extracts options from a set of arguments. Removes and returns the last
18
+ # element in the array if it's a hash, otherwise returns a blank hash.
19
+ #
20
+ # def options(*args)
21
+ # args.extract_options!
22
+ # end
23
+ #
24
+ # options(1, 2) # => {}
25
+ # options(1, 2, :a => :b) # => {:a=>:b}
26
+ def extract_options!
27
+ if last.is_a?(Hash) && last.extractable_options?
28
+ pop
29
+ else
30
+ {}
31
+ end
32
+ end
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,42 @@
1
+ module Shellter
2
+ module Runners
3
+ class Background < Base
4
+ def run(command, &block)
5
+ ensure_forking!
6
+
7
+ # io for child process
8
+ reader, writer = IO.pipe
9
+
10
+ # first fork, create a session leader
11
+ fork do
12
+ reader.close
13
+ Process.setsid
14
+
15
+ # second fork, create a detached child
16
+ # and write its pid to the writer pipe
17
+ result = fork do
18
+ Dir.chdir "/"
19
+ STDIN.reopen "/dev/null"
20
+ STDOUT.reopen "/dev/null", "a"
21
+ STDERR.reopen "/dev/null", "a"
22
+
23
+ exec command
24
+ end
25
+
26
+ # write the pid of the forked child to the pipe
27
+ writer.puts result
28
+ end
29
+
30
+ writer.close
31
+
32
+ yield StringIO.new, StringIO.new, StringIO.new, reader.gets.strip.to_i
33
+ end
34
+
35
+ private
36
+
37
+ def ensure_forking!
38
+ raise RuntimeError, "Cannot fork on this system!" unless Process.respond_to?(:fork)
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,13 @@
1
+ module Shellter
2
+ module Runners
3
+ class Base
4
+ def initialize(options = {})
5
+
6
+ end
7
+
8
+ def run(command, &block)
9
+ POpen4.popen4(command, &block)
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ module Shellter
2
+ module Runners
3
+ class Windows < Base
4
+ def run(command, mode = "b", &block)
5
+ POpen4.popen4(command, mode, &block)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,3 @@
1
+ module Shellter
2
+ VERSION = "0.9.0"
3
+ end
data/lib/shellter.rb ADDED
@@ -0,0 +1,17 @@
1
+ require 'escape'
2
+ require 'popen4'
3
+
4
+ require 'shellter/core_ext'
5
+
6
+ module Shellter
7
+ autoload :ClassMethods, "shellter/class_methods"
8
+ autoload :Command, "shellter/command"
9
+ autoload :VERSION, "shellter/version"
10
+
11
+ module Runners
12
+ autoload :Background, "shellter/runners/background"
13
+ autoload :Base, "shellter/runners/base"
14
+ end
15
+
16
+ extend ClassMethods
17
+ end
data/shellter.gemspec ADDED
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/shellter/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Andrew Eberbach"]
6
+ gem.email = ["andrew@ebertech.ca"]
7
+ gem.description = %q{A library to help with command line launching}
8
+ gem.summary = %q{A rounder wheel version of its predecessors.}
9
+ gem.homepage = "https://github.com/ebertech/shellter"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "shellter"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = Shellter::VERSION
17
+
18
+ gem.add_dependency "popen4"
19
+ gem.add_dependency "escape"
20
+
21
+ gem.add_development_dependency "pry"
22
+ gem.add_development_dependency "pry-doc"
23
+ gem.add_development_dependency "pry-nav"
24
+ gem.add_development_dependency "simplecov"
25
+ end
metadata ADDED
@@ -0,0 +1,156 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: shellter
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.9.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Andrew Eberbach
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-06-23 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: popen4
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: escape
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: pry
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry-doc
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: pry-nav
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: simplecov
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: A library to help with command line launching
111
+ email:
112
+ - andrew@ebertech.ca
113
+ executables: []
114
+ extensions: []
115
+ extra_rdoc_files: []
116
+ files:
117
+ - .gitignore
118
+ - Gemfile
119
+ - LICENSE
120
+ - README.md
121
+ - Rakefile
122
+ - lib/shellter.rb
123
+ - lib/shellter/class_methods.rb
124
+ - lib/shellter/command.rb
125
+ - lib/shellter/core_ext.rb
126
+ - lib/shellter/runners/background.rb
127
+ - lib/shellter/runners/base.rb
128
+ - lib/shellter/runners/windows.rb
129
+ - lib/shellter/version.rb
130
+ - shellter.gemspec
131
+ homepage: https://github.com/ebertech/shellter
132
+ licenses: []
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ! '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project:
151
+ rubygems_version: 1.8.24
152
+ signing_key:
153
+ specification_version: 3
154
+ summary: A rounder wheel version of its predecessors.
155
+ test_files: []
156
+ has_rdoc: