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