angry_shell 0.0.1

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