rubysh 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,17 @@
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
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rubysh.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Greg Brockman
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,88 @@
1
+ # Rubysh
2
+
3
+ Rubysh: Ruby subprocesses made easy
4
+
5
+ Rubysh makes shelling out easy with a __sh__-like syntax layer for Ruby:
6
+
7
+ irb -r rubysh
8
+ >> command = Rubysh('echo', 'hello-from-Rubysh') | Rubysh('grep', '--color', 'Rubysh')
9
+ >> command.run
10
+ hello-from-Rubysh
11
+ => Rubysh::Runner: echo hello-from-Rubysh | grep --color Rubysh (exitstatus: 0)
12
+
13
+ Rubysh philosophy is to make simple tasks simple and complex tasks
14
+ possible.
15
+
16
+ ## Motivation
17
+
18
+ Existing Ruby shell libaries make it very difficult to do tasks that
19
+ are simple in __sh__, such as:
20
+
21
+ - piping the output from one program to another
22
+ - redirecting a program's output to a file
23
+ - use a pre-tokenized array of arguments
24
+
25
+ Rubysh tries to emulate __sh__'s interface and semantics as closely as
26
+ possible.
27
+
28
+ ## Features
29
+
30
+ Redirecting a file descriptor to a file:
31
+
32
+ # echo hello-from-Rubysh >/tmp/file.txt
33
+ Rubysh('echo', 'hello-from-Rubysh', Rubysh.stdout > '/tmp/file.txt')
34
+ Rubysh('echo', 'hello-from-Rubysh', Rubysh::FD(1) > '/tmp/file.txt')
35
+
36
+ Redirecting a file descriptor to another file descriptor:
37
+
38
+ # echo hello-from-Rubysh 2>&1
39
+ Rubysh('echo', 'hello-from-Rubysh', Rubysh.stderr > Rubysh.stdout)
40
+
41
+ Feeding standard input with a string literal:
42
+
43
+ # cat <<< "hello there"
44
+ Rubysh('cat', Rubysh.<<< 'hello there')
45
+
46
+ Rubysh has been written to work with arbitrary file descriptors, so
47
+ you can do the same advanced FD redirection magic you can in __sh__:
48
+
49
+ # cat 3<<< "hello there" <&3
50
+ Rubysh('cat', Rubysh::FD(3).<<< 'hello there', Rubysh.stdin < Rubysh::FD(3))
51
+
52
+ You can also capture output to a named target (here :stdout, :stderr
53
+ are arbitrary symbols):
54
+
55
+ command = Rubysh('echo', 'hi', Rubysh.stdout > :stdout, Rubysh.stderr > :stderr)
56
+ runner = command.run
57
+ runner.data(:stdout) # "hi\n"
58
+ runner.data(:stderr) # ""
59
+
60
+ Support for controlled input isn't quite ready, but the syntax will be
61
+ similar to the above. I want to support interactivity (so being able
62
+ to write data, read some data, and then write more data), and haven't
63
+ quite decided on the right API for this yet.
64
+
65
+ ## API
66
+
67
+ The Rubysh helper function produces instances of __BaseCommand__. You
68
+ can run __run__ on these to spawn a subprocess and then __wait__ for
69
+ it to complete. Alternatively, you can do:
70
+
71
+ command = Rubysh('ls')
72
+ runner = command.run_async
73
+ runner.wait
74
+
75
+ ## Installation
76
+
77
+ Rubysh is hosted on Rubygems. You can install by adding this line to
78
+ your application's Gemfile:
79
+
80
+ gem 'rubysh'
81
+
82
+ Or by installing directly via
83
+
84
+ $ gem install rubysh
85
+
86
+ ## Contributing
87
+
88
+ Patches welcome! I'm happy to merge pull requests.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env rake
2
+ # require 'bundler/gem_tasks'
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs = ["lib"]
7
+ # t.warning = true
8
+ t.verbose = true
9
+ t.test_files = FileList['test/**/*.rb']
10
+ end
@@ -0,0 +1,65 @@
1
+ require 'shellwords'
2
+
3
+ module Rubysh
4
+ # TODO:
5
+ #
6
+ # - freeze after initialize?
7
+ class BaseCommand
8
+ def stringify_arg(arg)
9
+ case arg
10
+ when BaseCommand, BaseDirective
11
+ arg.stringify
12
+ else
13
+ Shellwords.shellescape(arg.to_s)
14
+ end
15
+ end
16
+
17
+ def to_s
18
+ "Command: #{stringify}"
19
+ end
20
+
21
+ def inspect
22
+ to_s
23
+ end
24
+
25
+ def create_runner
26
+ Runner.new(self)
27
+ end
28
+
29
+ def run
30
+ create_runner.run
31
+ end
32
+
33
+ def run_async
34
+ create_runner.run_async
35
+ end
36
+
37
+ def |(other)
38
+ raise NotImplementedError.new("Override in subclass")
39
+ end
40
+
41
+ def initialize(args)
42
+ raise NotImplementedError.new("Override in subclass")
43
+ end
44
+
45
+ def start_async(runner)
46
+ raise NotImplementedError.new("Override in subclass")
47
+ end
48
+
49
+ def wait(runner)
50
+ raise NotImplementedError.new("Override in subclass")
51
+ end
52
+
53
+ def stringify
54
+ raise NotImplementedError.new("Override in subclass")
55
+ end
56
+
57
+ def stdout=(value)
58
+ raise NotImplementedError.new("Override in subclass")
59
+ end
60
+
61
+ def stdin=(value)
62
+ raise NotImplementedError.new("Override in subclass")
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,24 @@
1
+ module Rubysh
2
+ class BaseDirective
3
+ def stringify
4
+ raise NotImplementedError.new("Override in subclass")
5
+ end
6
+
7
+ def prepare!(runner)
8
+ raise NotImplementedError.new("Override in subclass")
9
+ end
10
+
11
+ def apply_parent!(runner)
12
+ raise NotImplementedError.new("Override in subclass")
13
+ end
14
+
15
+ def apply!(runner)
16
+ raise NotImplementedError.new("Override in subclass")
17
+ end
18
+
19
+ # TODO: DRY up?
20
+ def state(runner)
21
+ runner.state(self)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,102 @@
1
+ module Rubysh
2
+ class Command < BaseCommand
3
+ attr_accessor :raw_args, :directives, :args
4
+
5
+ def initialize(args)
6
+ @raw_args = args
7
+ @directives = []
8
+ @args = nil
9
+
10
+ process_args
11
+ end
12
+
13
+ def process_args
14
+ @args = @raw_args.map do |arg|
15
+ case arg
16
+ when BaseCommand
17
+ raise NotImplementedError.new('Not ready for subshells yet')
18
+ when BaseDirective
19
+ @directives << arg
20
+ nil
21
+ else
22
+ arg.to_s
23
+ end
24
+ end.compact
25
+ end
26
+
27
+ def stringify
28
+ @raw_args.map do |arg|
29
+ stringify_arg(arg)
30
+ end.join(' ')
31
+ end
32
+
33
+ def |(other)
34
+ Pipeline.new([self, other])
35
+ end
36
+
37
+ def post_fork(runner, &blk)
38
+ extra_post_forks(runner) << blk
39
+ end
40
+
41
+ def set_stdout(runner, value)
42
+ directive = FD.new(:stdout) > value
43
+ add_directive(runner, directive)
44
+ end
45
+
46
+ def set_stdin(runner, value)
47
+ directive = FD.new(:stdin) < value
48
+ add_directive(runner, directive)
49
+ end
50
+
51
+ def status(runner)
52
+ state(runner)[:subprocess].status
53
+ end
54
+
55
+ def pid(runner)
56
+ state(runner)[:subprocess].pid
57
+ end
58
+
59
+ def prepare!(runner)
60
+ @directives.each {|directive| directive.prepare!(runner)}
61
+ end
62
+
63
+ def start_async(runner)
64
+ # Need to call this *after* we've set up pipeline
65
+ # PipeWrappers. Would prefer to call it in prepare!, but then
66
+ # we'd have to take care of closing the FDs in the parent
67
+ # process here anyway.
68
+ prepare_subprocess(runner)
69
+ state(runner)[:subprocess].run
70
+ end
71
+
72
+ def wait(runner)
73
+ state(runner)[:subprocess].wait
74
+ end
75
+
76
+ private
77
+
78
+ def add_directive(runner, directive)
79
+ extra_directives(runner) << directive
80
+ end
81
+
82
+ def state(runner)
83
+ runner.state(self)
84
+ end
85
+
86
+ def extra_directives(runner)
87
+ state(runner)[:extra_directives] ||= []
88
+ end
89
+
90
+ def extra_post_forks(runner)
91
+ state(runner)[:extra_post_forks] ||= []
92
+ end
93
+
94
+ def prepare_subprocess(runner)
95
+ # extras first because they are currently only used for
96
+ # pipeline, which should not win out over internal redirects.
97
+ directives = extra_directives(runner) + @directives
98
+ post_forks = extra_post_forks(runner)
99
+ state(runner)[:subprocess] = Subprocess.new(args, directives, post_forks, runner)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,20 @@
1
+ module Rubysh
2
+ module Error
3
+ class BaseError < Exception; end
4
+
5
+ class ExecError < BaseError
6
+ # Exception klass and caller from the child process
7
+ attr_accessor :klass, :caller
8
+
9
+ def initialize(message, klass, caller)
10
+ super(message)
11
+ @klass = klass
12
+ @caller = caller
13
+ end
14
+ end
15
+
16
+ class UnreachableError < BaseError; end
17
+ class AlreadyClosedError < BaseError; end
18
+ class AlreadyRunError < BaseError; end
19
+ end
20
+ end
data/lib/rubysh/fd.rb ADDED
@@ -0,0 +1,43 @@
1
+ module Rubysh
2
+ class FD
3
+ attr_accessor :fileno
4
+
5
+ def initialize(fileno)
6
+ case fileno
7
+ when Integer
8
+ # pass
9
+ when :stdin
10
+ fileno = 0
11
+ when :stdout
12
+ fileno = 1
13
+ when :stderr
14
+ fileno = 2
15
+ else
16
+ raise Rubysh::Error::BaseError.new("Fileno must be an integer or one of :stdin, :stdout, :stderr, not #{fileno.inspect}")
17
+ end
18
+
19
+ @fileno = fileno
20
+ end
21
+
22
+ def >(target)
23
+ Redirect.new(self, '>', target)
24
+ end
25
+
26
+ def <(target)
27
+ Redirect.new(self, '<', target)
28
+ end
29
+
30
+ def <<
31
+ Rubysh.<<(self)
32
+ end
33
+
34
+ def to_s
35
+ "FD: #{@fileno}"
36
+ end
37
+
38
+ def ==(other)
39
+ self.class == other.class &&
40
+ self.fileno == other.fileno
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,4 @@
1
+ module Rubysh
2
+ class Pipe < Directive
3
+ end
4
+ end
@@ -0,0 +1,70 @@
1
+ module Rubysh
2
+ class Pipeline < BaseCommand
3
+ attr_accessor :pipeline
4
+
5
+ def initialize(pipeline)
6
+ raise Rubysh::Error::BaseError.new("Cannot create an empty pipeline") if pipeline.length == 0
7
+ @pipeline = pipeline
8
+ end
9
+
10
+ # sh semantics are that your exitstatus is that of the last in the
11
+ # pipeline
12
+ def status(runner)
13
+ @pipeline[-1].status(runner)
14
+ end
15
+
16
+ def pid(runner)
17
+ @pipeline[-1].pid(runner)
18
+ end
19
+
20
+ def prepare!(runner)
21
+ @pipeline.each {|command| command.prepare!(runner)}
22
+ end
23
+
24
+ def pipeline_pairs
25
+ @pipeline[0...-1].zip(@pipeline[1..-1])
26
+ end
27
+
28
+ def stringify
29
+ @pipeline.map {|cmd| cmd.stringify}.join(' | ')
30
+ end
31
+
32
+ def |(other)
33
+ self.class.new(pipeline + [other])
34
+ end
35
+
36
+ def start_async(runner)
37
+ last_pipe = nil
38
+
39
+ pipeline_pairs.each do |left, right|
40
+ # TODO: maybe create an object to represent the pipe
41
+ # relationship, instead of manually assembling here.
42
+ #
43
+ # Don't want to have more than 2 pipes open at a time, so need
44
+ # to #run_async and #close here.
45
+ pipe = Subprocess::PipeWrapper.new
46
+ setup_pipe(runner, pipe, left, right)
47
+
48
+ left.start_async(runner)
49
+ last_pipe.close if last_pipe
50
+ last_pipe = pipe
51
+ end
52
+
53
+ @pipeline[-1].start_async(runner)
54
+ last_pipe.close if last_pipe
55
+ end
56
+
57
+ def setup_pipe(runner, pipe, left, right)
58
+ left.set_stdout(runner, pipe.writer)
59
+ right.set_stdin(runner, pipe.reader)
60
+ end
61
+
62
+ def wait(runner)
63
+ # It's likely we should actually wait for these in parallel; I'm
64
+ # not really sure right now. Might be tricky to avoid waiting
65
+ # for other processes run by this program (could probably use
66
+ # process groups for that?)
67
+ @pipeline.each {|cmd| cmd.wait(runner)}
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,181 @@
1
+ module Rubysh
2
+ # Note that in bash, the semantics of redirection appear to be
3
+ # following (tested empirically, rather than reading a spec):
4
+ #
5
+ # - [a<&b] and [a>&b] mean the same thing: copy FD b to a
6
+ # (try 'echo test 3>/tmp/testing.txt 1<&3')
7
+ # - [a<&a] appears to be a no-op: ls /dev/fd 9<&9
8
+ # - If b != a is an invalid file descriptor, then [a>&b] throws an
9
+ # error.
10
+ # - Pathnames can only be on the right-hand side of a redirect.
11
+ class Redirect < BaseDirective
12
+ VALID_DIRECTIONS = ['<', '>', '>>']
13
+
14
+ attr_accessor :source, :direction, :target
15
+
16
+ def initialize(source, direction, target)
17
+ unless VALID_DIRECTIONS.include?(direction)
18
+ raise Rubysh::Error::BaseError.new("Direction must be one of #{VALID_DIRECTIONS.join(', ')}, not #{direction.inspect}")
19
+ end
20
+
21
+ unless source.kind_of?(IO) || source.kind_of?(FD) || source.kind_of?(Integer)
22
+ raise Rubysh::Error::BaseError.new("Invalid source: #{source.inspect}. Source must be an IO, a Rubysh::FD, or an Integer.")
23
+ end
24
+
25
+ unless target.respond_to?(:fileno) || target.kind_of?(Integer) || target.kind_of?(String) || target.kind_of?(Symbol)
26
+ raise Rubysh::Error::BaseError.new("Invalid target: #{target.inspect}. Target must respond to :fileno or be an Integer, a String, or a Symbol.")
27
+ end
28
+
29
+ @source = source
30
+ @target = target
31
+ @direction = direction
32
+ end
33
+
34
+ def printable_source
35
+ Util.to_fileno(source)
36
+ end
37
+
38
+ def printable_target
39
+ case target
40
+ when Symbol
41
+ target.inspect
42
+ else
43
+ Util.to_fileno(target)
44
+ end
45
+ end
46
+
47
+ # TODO: support files
48
+ def stringify
49
+ source_file = printable_source
50
+ target_file = printable_target
51
+
52
+ case direction
53
+ when '<', '>>'
54
+ source_file = nil if source_file == 0
55
+ when '>'
56
+ source_file = nil if source_file == 1
57
+ else
58
+ raise Rubysh::Error::BaseError.new("Unrecognized direction: #{direction.inspect}")
59
+ end
60
+
61
+ ampersand = target_file.kind_of?(Integer) ? '&' : nil
62
+
63
+ "#{source_file}#{direction}#{ampersand}#{target_file}"
64
+ end
65
+
66
+ def to_s
67
+ "Redirect: #{stringify}"
68
+ end
69
+
70
+ def ==(other)
71
+ self.class == other.class &&
72
+ self.printable_source == other.printable_source &&
73
+ self.printable_target == other.printable_target
74
+ end
75
+
76
+ def reading?
77
+ direction == '<'
78
+ end
79
+
80
+ def target_reading?
81
+ !reading?
82
+ end
83
+
84
+ def writing?
85
+ !reading?
86
+ end
87
+
88
+ def target_writing?
89
+ !writing?
90
+ end
91
+
92
+ def truncate?
93
+ direction == '>'
94
+ end
95
+
96
+ def named_target?
97
+ target.kind_of?(Symbol)
98
+ end
99
+
100
+ def target_name
101
+ raise Rubysh::Error::BaseError.new("Not a named target") unless named_target?
102
+ target
103
+ end
104
+
105
+ def prepare!(runner)
106
+ prepare_target(runner)
107
+ end
108
+
109
+ def prepare_target(runner)
110
+ return unless named_target?
111
+ targets = runner.targets
112
+ if targets.include?(target_name)
113
+ raise Rubysh::Error::BaseError.new("#{runner} already has a named target: #{target_name.inspect}")
114
+ end
115
+
116
+ pipe = Subprocess::PipeWrapper.new
117
+
118
+ targets[target_name] = {
119
+ :target_reading? => target_reading?,
120
+ :target => target_reading? ? pipe.reader : pipe.writer,
121
+ :complement => target_reading? ? pipe.writer : pipe.reader,
122
+ :buffer => []
123
+ }
124
+ end
125
+
126
+ # E.g. Rubysh.stdin < :stdin
127
+ def apply_parent!(runner)
128
+ return unless named_target?
129
+ target_state = runner.target_state(target_name)
130
+ target_state[:complement].close
131
+ end
132
+
133
+ def apply!(runner)
134
+ Rubysh.log.info("About to apply #{self} for #{$$}")
135
+
136
+ # Open the target
137
+ target_io = file_as_io(runner, target)
138
+
139
+ target_fd = Util.to_fileno(target_io)
140
+ source_fd = Util.to_fileno(source)
141
+
142
+ # Copy target -> source
143
+ Util.dup2(target_fd, source_fd)
144
+ Util.set_cloexec(source_fd, false)
145
+ end
146
+
147
+ private
148
+
149
+ # If providing your own open FD, you have to set cloexec yourself.
150
+ def file_as_io(runner, file, default_to_cloexec=true)
151
+ return file if file.kind_of?(IO)
152
+ # If it's an FD, canonicalize to the FD number
153
+ file = Util.to_fileno(file)
154
+
155
+ if file.kind_of?(Integer)
156
+ io = Util.io_without_autoclose(file)
157
+ # Someone else opened
158
+ default_to_cloexec = false
159
+ elsif file.kind_of?(String) && reading?
160
+ io = File.open(file)
161
+ elsif file.kind_of?(String) && writing? && truncate?
162
+ # Make the following cases explicit for future compatability
163
+ # (also to make it clear on an exception which case is at
164
+ # fault).
165
+ io = File.open(file, 'w')
166
+ elsif file.kind_of?(String) && writing? && !truncate?
167
+ io = File.open(file, 'a')
168
+ elsif file.kind_of?(Symbol)
169
+ target_state = runner.target_state(file)
170
+ io = target_state[:complement]
171
+ # Someone else opened
172
+ default_to_cloexec = false
173
+ else
174
+ raise Rubysh::Error::BaseError.new("Unrecognized file spec: #{file.inspect}")
175
+ end
176
+
177
+ io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) if default_to_cloexec
178
+ io
179
+ end
180
+ end
181
+ end