angry_shell 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
4
+ .*.sw?
data/Gemfile ADDED
@@ -0,0 +1,8 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in angry_shell.gemspec
4
+ gemspec
5
+
6
+ group :test do
7
+ gem 'exemplor'
8
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,16 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ angry_shell (0.0.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ exemplor (3000.3.0)
10
+
11
+ PLATFORMS
12
+ ruby
13
+
14
+ DEPENDENCIES
15
+ angry_shell!
16
+ exemplor
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
data/Readme.md ADDED
@@ -0,0 +1,51 @@
1
+ # AngryShell
2
+
3
+ `AngryShell` makes you less angry about running shell commands from ruby.
4
+
5
+ # Usage
6
+ require 'angry_shell'
7
+ include AngryShell::ShellMethods
8
+
9
+ sh("echo Hello World").to_s #=> 'Hello World'
10
+ sh("echo Hello World").ok? #=> true
11
+ sh("echo Hello World").run #=> nil
12
+
13
+ # the command 'schmortle' doesn't exist :)
14
+ sh("schmortle").to_s #=> ''
15
+ sh("schmortle").ok? #=> false
16
+ sh("schmortle").run #=> raises Errno::ENOENT, since schmortle doesn't exist
17
+
18
+ begin
19
+ sh("sh -c 'echo hello && exit 1'").run
20
+ rescue AngryShell::ShellError
21
+ $!.result.stdout #=> "hello\n"
22
+ $!.result.ok? #=> false
23
+ end
24
+
25
+ ## Duck punching
26
+
27
+ require 'angry_shell'
28
+ "echo Hello World".sh.to_s #=> 'Hello World'
29
+
30
+ ## License
31
+
32
+ Copyright (c) 2010 Lachie Cox
33
+
34
+ Permission is hereby granted, free of charge, to any person obtaining a copy
35
+ of this software and associated documentation files (the "Software"), to deal
36
+ in the Software without restriction, including without limitation the rights
37
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
38
+ copies of the Software, and to permit persons to whom the Software is
39
+ furnished to do so, subject to the following conditions:
40
+
41
+ The above copyright notice and this permission notice shall be included in
42
+ all copies or substantial portions of the Software.
43
+
44
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
45
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
46
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
47
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
48
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
49
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
50
+ THE SOFTWARE.
51
+
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ # $:.push File.expand_path("../lib", __FILE__)
3
+ # require "angry_shell/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "angry_shell"
7
+ s.version = '0.0.1' #AngryShell::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Lachie Cox"]
10
+ s.email = ["lachie.cox@plus2.com.au"]
11
+ s.homepage = "http://rubygems.org/gems/angry_shell"
12
+ s.summary = %q{Shell}
13
+ s.description = %q{}
14
+
15
+ s.rubyforge_project = "angry_shell"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'bundler'
3
+ Bundler.setup :default, :test
4
+
5
+ $: << File.expand_path('../../lib',__FILE__)
6
+
7
+ require 'angry_shell'
8
+ require 'exemplor'
@@ -0,0 +1,66 @@
1
+ require 'eg.helper'
2
+
3
+ eg 'popen4 - normal, exahust io' do
4
+ out = nil
5
+ AngryShell::Shell.new.popen4(:cmd => 'echo Hello') do |cid,ipc|
6
+ out = ipc.stdout.read
7
+ end
8
+
9
+ Assert(out == "Hello\n")
10
+ end
11
+
12
+ eg 'popen4 - normal, stream io' do
13
+ out = nil
14
+ AngryShell::Shell.new.popen4(:cmd => 'echo Hello', :stream => true) do |cid,ipc|
15
+ out = ipc.stdout.read
16
+ end
17
+
18
+ Assert(out == "Hello\n")
19
+ end
20
+
21
+ eg 'popen4 - error' do
22
+ begin
23
+ AngryShell::Shell.new.popen4(:cmd => 'casplortleecho Hello') do |cid,ipc|
24
+ end
25
+ raised = false
26
+ rescue Errno::ENOENT
27
+ raised = true
28
+ end
29
+
30
+ Assert(raised)
31
+ end
32
+
33
+ eg 'run' do
34
+ AngryShell::Shell.new("echo Whats happening").run
35
+ Assert( :didnt_raise )
36
+ end
37
+
38
+ eg 'ok?' do
39
+ Assert( AngryShell::Shell.new("echo Whats happening").ok? )
40
+ end
41
+
42
+
43
+ eg 'to_s' do
44
+ Assert( AngryShell::Shell.new("echo -n Whats happening").to_s == "Whats happening" )
45
+ end
46
+
47
+ eg.helpers do
48
+ include AngryShell::ShellMethods
49
+ end
50
+
51
+ eg 'helper' do
52
+ Assert( sh("echo Something").to_s == "Something" )
53
+ end
54
+
55
+ eg 'helper - error' do
56
+ raised = false
57
+ begin
58
+ sh("sh -c 'echo hello && exit 1'").run
59
+ rescue AngryShell::ShellError
60
+ raised = true
61
+ Assert( $!.result.stdout == "hello\n" )
62
+ Assert( ! $!.result.ok? )
63
+ end
64
+
65
+ Assert( raised )
66
+ end
@@ -0,0 +1,3 @@
1
+ module AngryShell
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,402 @@
1
+ ## AngryShell
2
+ # `AngryShell` makes you less angry about running shell commands.
3
+ #
4
+ # AngryShell is extracted from YesMaster's CommonMob. Contains code adapted from Chef and open4.
5
+
6
+ require 'fcntl'
7
+ require 'stringio'
8
+ require 'pp'
9
+
10
+ module AngryShell
11
+ class ShellError < StandardError
12
+ attr_accessor :result
13
+
14
+ def initialize(msg,result)
15
+ @result = result
16
+ super(msg)
17
+ end
18
+ end
19
+
20
+ module ShellMethods
21
+ def sh(*args,&blk)
22
+ AngryShell::Shell.new(*args,&blk)
23
+ end
24
+
25
+ # call ruby, or a ruby command *without* the environment being cleaned of bundler spooge
26
+ def bundler_sh(*args,&blk)
27
+ args.options.without_cleaning_bundler = true
28
+ AngryShell::Shell.new(*args,&blk)
29
+ end
30
+ end
31
+
32
+ class Shell
33
+ def debug(*msg)
34
+ puts "sh: #{msg * ' '}"
35
+ end
36
+
37
+ attr_reader :options
38
+
39
+ def initialize(*args,&block)
40
+ @block = block
41
+ @options = if Hash === args.last then args.pop else {} end
42
+
43
+ unless args.empty?
44
+ @options[:cmd] = args
45
+ end
46
+
47
+ @options[:stream] = false unless @options.key?(:stream)
48
+ end
49
+
50
+ def execute
51
+ error,out = nil,nil
52
+
53
+ rv = popen4(options) {|pid,ipc|
54
+ out = ipc.stdout.read
55
+ error = ipc.stderr.read
56
+ }
57
+
58
+ rv.stderr = error
59
+ rv.stdout = out
60
+
61
+ rv
62
+ end
63
+
64
+ # runs the command, raising if it doesn't return success.
65
+ def run
66
+ execute.ensure_ok!
67
+ end
68
+
69
+ # runs the command, returning true if it returns success.
70
+ def ok?
71
+ execute.ok?
72
+ end
73
+
74
+ # runs the command, returning its `stdout`. If the command doesn't return success, return a blank string.
75
+ def to_s
76
+ result = execute
77
+ if result.ok?
78
+ result.stdout.chomp
79
+ else
80
+ ''
81
+ end
82
+ end
83
+
84
+ # We encapsulate the shell's result, including the Process::Status, stdout and stderr.
85
+ class ShellResult < Struct.new(:process_result, :options, :stderr, :stdout)
86
+ def ok?
87
+ process_result.success?
88
+ end
89
+
90
+ def ensure_ok!
91
+ unless ok?
92
+ raise ShellError.new("unable to run command\ncommand=#{options[:cmd]}\noptions=#{options.pretty_inspect}\noutput=#{stdout}\nerror=#{stderr}",self)
93
+ end
94
+ end
95
+ end
96
+
97
+ class IPCState < Struct.new(:write,:read,:error,:exception)
98
+ def initialize
99
+ super(IO.pipe, IO.pipe, IO.pipe, IO.pipe)
100
+ end
101
+
102
+ def before_fork!
103
+ exception.last.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC)
104
+ end
105
+
106
+ def child_after_fork!
107
+ write.last.close
108
+ STDIN.reopen write.first
109
+ write.first.close
110
+
111
+ self.write = STDIN
112
+
113
+ read.first.close
114
+ STDOUT.reopen read.last
115
+ read.last.close
116
+
117
+ self.read = STDOUT
118
+
119
+ error.first.close
120
+ STDERR.reopen error.last
121
+ error.last.close
122
+
123
+ self.error = STDERR
124
+
125
+ exception.first.close
126
+ self.exception = exception.last
127
+
128
+ STDOUT.sync = STDERR.sync = true
129
+ end
130
+
131
+ def parent_after_fork!
132
+ [write.first, read.last, error.last, exception.last].each {|fd| fd.close}
133
+
134
+ self.write = write.last
135
+ self.read = read.first
136
+ self.error = error.first
137
+ self.exception = exception.first
138
+ end
139
+
140
+ alias :stdout :read
141
+ alias :stderr :error
142
+
143
+ def close_all
144
+ [ read, write, error, exception ].flatten.compact.each {|fd| fd.close unless fd.closed?}
145
+ end
146
+
147
+ end
148
+
149
+ # This is taken from Chef and rewritten.
150
+ #
151
+ # Chef's preamble:
152
+ # This is taken directly from Ara T Howard's Open4 library, and then
153
+ # modified to suit the needs of Chef. Any bugs here are most likely
154
+ # my own, and not Ara's.
155
+ #
156
+ # The original appears in external/open4.rb in its unmodified form.
157
+ #
158
+ # Thanks Ara!
159
+ def popen4(args={}, &blk)
160
+ popen4_normalise_args(args)
161
+
162
+
163
+ # We pass and manipulate all IPC pipes around inside this object.
164
+ ipc = IPCState.new
165
+
166
+ verbose = $VERBOSE
167
+ cid = begin
168
+ $VERBOSE = nil
169
+ ipc.before_fork!
170
+
171
+ fork {
172
+ popen4_proceed_as_child(args,ipc)
173
+ }
174
+ ensure
175
+ $VERBOSE = verbose
176
+ end
177
+
178
+ popen4_proceed_as_parent(cid,args,ipc,&blk)
179
+ end
180
+
181
+
182
+ def popen4_proceed_as_parent(cid,args,ipc,&blk)
183
+ ipc.parent_after_fork!
184
+
185
+ # The first thing a parent does after forking is look for an Marshalled exception on the exception pipe.
186
+ begin
187
+ e = Marshal.load ipc.exception
188
+ raise(Exception === e ? e : "unknown failure!")
189
+ rescue EOFError # If we get an EOF error, then the exec was successful
190
+ 42
191
+ ensure
192
+ ipc.exception.close
193
+ end
194
+
195
+ ipc.write.sync = true
196
+
197
+ if block_given?
198
+ begin
199
+ if args[:stream]
200
+ # hand the block the pipes inside ipc to manipulate manually
201
+ yield(cid, ipc)
202
+ ShellResult.new(Process.waitpid2(cid).last, args)
203
+ else
204
+ popen4_parent_exhaust_io(cid,args,ipc,&blk)
205
+ end
206
+ ensure
207
+ ipc.close_all
208
+ end
209
+ else
210
+ # Return the pipes. The User needs to clean up after themselves.
211
+ [cid, ipc]
212
+ end
213
+ end
214
+
215
+ # Use select to read the entire contents of the pipes into StringIOs.
216
+ # This is the main change to come from Chef vs the original open4.
217
+ def popen4_parent_exhaust_io(cid,args,ipc,&blk)
218
+ output = StringIO.new
219
+ error = StringIO.new
220
+
221
+ if args[:input]
222
+ ipc.write.puts args[:input]
223
+ end
224
+
225
+ ipc.write.close
226
+
227
+ stdout = ipc.read
228
+ stderr = ipc.error
229
+
230
+ stdout.sync = true
231
+ stderr.sync = true
232
+
233
+ stdout.fcntl(Fcntl::F_SETFL, stdout.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
234
+ stderr.fcntl(Fcntl::F_SETFL, stderr.fcntl(Fcntl::F_GETFL) | Fcntl::O_NONBLOCK)
235
+
236
+ stdout_finished = false
237
+ stderr_finished = false
238
+
239
+ results = nil
240
+
241
+ while !stdout_finished && !stderr_finished
242
+ begin
243
+ channels_to_watch = []
244
+ channels_to_watch << stdout if !stdout_finished
245
+ channels_to_watch << stderr if !stderr_finished
246
+
247
+ ready = IO.select(channels_to_watch, nil, nil, 1.0)
248
+ rescue Errno::EAGAIN
249
+ results = Process.waitpid2(cid, Process::WNOHANG)
250
+
251
+ if results
252
+ stdout_finished = true
253
+ stderr_finished = true
254
+ end
255
+ end
256
+
257
+ if ready && ready.first.include?(stdout)
258
+ line = results ? stdout.gets(nil) : stdout.gets
259
+ if line
260
+ output.write(line)
261
+ else
262
+ stdout_finished = true
263
+ end
264
+ end
265
+
266
+ if ready && ready.first.include?(stderr)
267
+ line = results ? stderr.gets(nil) : stderr.gets
268
+ if line
269
+ error.write(line)
270
+ else
271
+ stderr_finished = true
272
+ end
273
+ end
274
+ end
275
+
276
+ results = Process.waitpid2(cid) unless results
277
+
278
+ output.rewind
279
+ error.rewind
280
+
281
+ ipc.read = output
282
+ ipc.error = error
283
+
284
+ blk[cid, ipc]
285
+
286
+ ShellResult.new(results.last, args)
287
+ end
288
+
289
+
290
+ def popen4_proceed_as_child(args,ipc)
291
+ ipc.child_after_fork!
292
+
293
+ if args[:group]
294
+ Process.egid = args[:group]
295
+ Process.gid = args[:group]
296
+ end
297
+
298
+ if args[:user]
299
+ Process.euid = args[:user]
300
+ Process.uid = args[:user]
301
+ end
302
+
303
+ # Copy the specified environment across to the child's environment.
304
+ # Keys with `nil` values are deleted from the environment.
305
+ args[:environment].each do |key,value|
306
+ if value.nil?
307
+ ENV.delete(key.to_s)
308
+ else
309
+ ENV[key.to_s] = value
310
+ end
311
+ end
312
+
313
+ if args[:umask]
314
+ umask = ((args[:umask].respond_to?(:oct) ? args[:umask].oct : args[:umask].to_i) & 007777)
315
+ File.umask(umask)
316
+ end
317
+
318
+ if args[:cwd]
319
+ Dir.chdir args[:cwd]
320
+ end
321
+
322
+ begin
323
+ cmd = args[:cmd]
324
+ if cmd.kind_of?(Array)
325
+ exec(*cmd)
326
+ else
327
+ exec(cmd)
328
+ end
329
+
330
+ raise 'forty-two'
331
+ rescue Object => e
332
+ Marshal.dump(e, ipc.exception)
333
+ ipc.exception.flush
334
+ end
335
+
336
+ ipc.exception.close unless (ipc.exception.closed?)
337
+ exit!
338
+ end
339
+
340
+ def popen4_normalise_args(args)
341
+ # Do we wait for the child process to die before we yield
342
+ # to the block, or after?
343
+ #
344
+ # By default, we are waiting before we yield the block.
345
+ args[:stream] ||= false
346
+
347
+
348
+ args[:user] ||= nil
349
+ unless args[:user].kind_of?(Integer)
350
+ args[:user] = Etc.getpwnam(args[:user]).uid if args[:user]
351
+ end
352
+
353
+ args[:group] ||= nil
354
+ unless args[:group].kind_of?(Integer)
355
+ args[:group] = Etc.getgrnam(args[:group]).gid if args[:group]
356
+ end
357
+
358
+ args[:environment] ||= {}
359
+
360
+ # Default on C locale so parsing commands output can be done
361
+ # independently of the node's default locale.
362
+ # "LC_ALL" could be set to nil, in which case we also must ignore it.
363
+ unless args[:environment].has_key?("LC_ALL")
364
+ args[:environment]["LC_ALL"] = "C"
365
+ end
366
+
367
+ unless TrueClass === args[:without_cleaning_bundler]
368
+ args[:environment].update('RUBYOPT' => nil, 'BUNDLE_GEMFILE' => nil, 'GEM_HOME' => nil, 'GEM_PATH' => nil)
369
+ end
370
+
371
+ # `:as` - run the command as another user, via sudo,
372
+ if user = args[:as]
373
+ if (evars = args[:environment].reject {|k,v| v.nil?}.map {|k,v| "#{k}=#{v}"}) && !evars.empty?
374
+ env = "env #{evars.join(' ')}"
375
+ else
376
+ env = ''
377
+ end
378
+
379
+ args[:cmd] = "sudo -H -u #{user} #{env} #{args[:cmd]}"
380
+ end
381
+ end
382
+
383
+ def massaged_args args
384
+ args.dup.tap do |args_to_print|
385
+ args_to_print[:environment] = e = args[:environment].dup
386
+
387
+ %w{LC_ALL GEM_HOME GEM_PATH RUBYOPT BUNDLE_GEMFILE}.each {|env| e.delete(env)}
388
+
389
+ args_to_print['cwd'] = args_to_print['cwd'].to_s if args_to_print['cwd']
390
+
391
+ args_to_print.delete_if{|k,v| v.blank?}
392
+ end
393
+ end
394
+
395
+ end
396
+ end
397
+
398
+ class String
399
+ def sh(options={})
400
+ AngryShell::Shell.new(self,options)
401
+ end
402
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: angry_shell
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Lachie Cox
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-10-24 00:00:00 +11:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: ""
22
+ email:
23
+ - lachie.cox@plus2.com.au
24
+ executables: []
25
+
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - .gitignore
32
+ - Gemfile
33
+ - Gemfile.lock
34
+ - Rakefile
35
+ - Readme.md
36
+ - angry_shell.gemspec
37
+ - examples/eg.helper.rb
38
+ - examples/usage.eg.rb
39
+ - lib/angry_shell.rb
40
+ - lib/angry_shell/version.rb
41
+ has_rdoc: true
42
+ homepage: http://rubygems.org/gems/angry_shell
43
+ licenses: []
44
+
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ segments:
55
+ - 0
56
+ version: "0"
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ segments:
62
+ - 0
63
+ version: "0"
64
+ requirements: []
65
+
66
+ rubyforge_project: angry_shell
67
+ rubygems_version: 1.3.6
68
+ signing_key:
69
+ specification_version: 3
70
+ summary: Shell
71
+ test_files: []
72
+