albino 1.2.3 → 1.3.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/albino.gemspec +6 -4
- data/lib/albino.rb +5 -3
- data/lib/albino/multi.rb +98 -0
- data/test/albino_test.rb +4 -3
- data/test/multi_albino_test.rb +58 -0
- data/vendor/multipygmentize +52 -0
- metadata +30 -8
- data/lib/albino/process.rb +0 -294
- data/test/process_test.rb +0 -102
data/albino.gemspec
CHANGED
@@ -13,8 +13,8 @@ Gem::Specification.new do |s|
|
|
13
13
|
## If your rubyforge_project name is different, then edit it and comment out
|
14
14
|
## the sub! line in the Rakefile
|
15
15
|
s.name = 'albino'
|
16
|
-
s.version = '1.
|
17
|
-
s.date = '2011-
|
16
|
+
s.version = '1.3.0'
|
17
|
+
s.date = '2011-03-03'
|
18
18
|
s.rubyforge_project = 'albino'
|
19
19
|
|
20
20
|
## Make sure your summary is short. The description may be as long
|
@@ -33,6 +33,7 @@ Gem::Specification.new do |s|
|
|
33
33
|
## require 'NAME.rb' or'/lib/NAME/file.rb' can be as require 'NAME/file.rb'
|
34
34
|
s.require_paths = %w[lib]
|
35
35
|
|
36
|
+
s.add_dependency('posix-spawn')
|
36
37
|
s.add_development_dependency('mocha')
|
37
38
|
|
38
39
|
## Leave this section as-is. It will be automatically generated from the
|
@@ -45,9 +46,10 @@ Gem::Specification.new do |s|
|
|
45
46
|
Rakefile
|
46
47
|
albino.gemspec
|
47
48
|
lib/albino.rb
|
48
|
-
lib/albino/
|
49
|
+
lib/albino/multi.rb
|
49
50
|
test/albino_test.rb
|
50
|
-
test/
|
51
|
+
test/multi_albino_test.rb
|
52
|
+
vendor/multipygmentize
|
51
53
|
]
|
52
54
|
# = MANIFEST =
|
53
55
|
|
data/lib/albino.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
|
-
require '
|
1
|
+
require 'posix-spawn'
|
2
|
+
|
2
3
|
##
|
3
4
|
# Wrapper for the Pygments command line tool, pygmentize.
|
4
5
|
#
|
@@ -44,8 +45,9 @@ require 'albino/process'
|
|
44
45
|
#
|
45
46
|
class Albino
|
46
47
|
class ShellArgumentError < ArgumentError; end
|
48
|
+
include POSIX::Spawn
|
47
49
|
|
48
|
-
VERSION = '1.
|
50
|
+
VERSION = '1.3.0'
|
49
51
|
|
50
52
|
class << self
|
51
53
|
attr_accessor :bin, :default_encoding, :timeout_threshold
|
@@ -69,7 +71,7 @@ class Albino
|
|
69
71
|
proc_options[:timeout] = options.delete(:timeout) || self.class.timeout_threshold
|
70
72
|
command = convert_options(options)
|
71
73
|
command.unshift(bin)
|
72
|
-
|
74
|
+
Child.new(*(command + [proc_options.merge(:input => write_target)]))
|
73
75
|
end
|
74
76
|
|
75
77
|
def colorize(options = {})
|
data/lib/albino/multi.rb
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'posix-spawn'
|
2
|
+
|
3
|
+
class Albino
|
4
|
+
if !const_defined?(:ShellArgumentError)
|
5
|
+
class ShellArgumentError < ArgumentError; end
|
6
|
+
end
|
7
|
+
|
8
|
+
# Wrapper for a custom multipygmentize script. Passes multiple code
|
9
|
+
# fragments in STDIN to Python. This assumes both Python and pygments are
|
10
|
+
# installed.
|
11
|
+
#
|
12
|
+
# Use like so:
|
13
|
+
#
|
14
|
+
# @syntaxer = Albino::Multi.new([ [:ruby, File.open("/some/file.rb")] ])
|
15
|
+
# puts @syntaxer.colorize
|
16
|
+
#
|
17
|
+
# It takes an Array of two-element arrays [lexer, code].
|
18
|
+
#
|
19
|
+
# You can also use Albino::Multi as a drop-in replacement. It currently has
|
20
|
+
# a few limitations however:
|
21
|
+
#
|
22
|
+
# * Only the HTML output format is supported.
|
23
|
+
# * UTF-8 encoding is forced.
|
24
|
+
#
|
25
|
+
# The default lexer is 'text'. You need to specify a lexer yourself;
|
26
|
+
# because we are using STDIN there is no auto-detect.
|
27
|
+
#
|
28
|
+
# To see all lexers and formatters available, run `pygmentize -L`.
|
29
|
+
class Multi
|
30
|
+
include POSIX::Spawn
|
31
|
+
|
32
|
+
class << self
|
33
|
+
attr_accessor :bin, :timeout_threshold
|
34
|
+
end
|
35
|
+
|
36
|
+
self.timeout_threshold = 10
|
37
|
+
self.bin = File.join(File.dirname(__FILE__), *%w(.. .. vendor multipygmentize))
|
38
|
+
|
39
|
+
# Initializes a new Albino::Multi and runs #colorize.
|
40
|
+
def self.colorize(*args)
|
41
|
+
new(*args).colorize
|
42
|
+
end
|
43
|
+
|
44
|
+
# This method accepts two forms of input:
|
45
|
+
#
|
46
|
+
# DEFAULT
|
47
|
+
#
|
48
|
+
# target - The Array of two-element [lexer, code] Arrays:
|
49
|
+
# lexer - The String lexer for the upcoming block of code.
|
50
|
+
# code - The String block of code to highlight.
|
51
|
+
#
|
52
|
+
# LEGACY
|
53
|
+
#
|
54
|
+
# target - The String block of code to highlight.
|
55
|
+
# lexer - The String lexer for the block of code.
|
56
|
+
#
|
57
|
+
# Albino#initialize also takes format and encoding which are ignored.
|
58
|
+
def initialize(target, lexer = :text, *args)
|
59
|
+
@spec = case target
|
60
|
+
when Array
|
61
|
+
@multi = true
|
62
|
+
target
|
63
|
+
else
|
64
|
+
[[lexer, target]]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
# Colorizes the code blocks.
|
69
|
+
#
|
70
|
+
# options - Specify options for the child process:
|
71
|
+
# timeout - A Fixnum timeout for the child process.
|
72
|
+
#
|
73
|
+
# Returns an Array of HTML highlighted code block Strings if an array of
|
74
|
+
# targets are given to #initialize, or just a single HTML highlighted code
|
75
|
+
# block String.
|
76
|
+
def colorize(options = {})
|
77
|
+
options[:timeout] ||= self.class.timeout_threshold
|
78
|
+
options[:input] = @spec.inject([]) do |memo, (lexer, code)|
|
79
|
+
memo << lexer << SEPARATOR
|
80
|
+
|
81
|
+
if code.respond_to?(:read)
|
82
|
+
out = code.read
|
83
|
+
code.close
|
84
|
+
code = out
|
85
|
+
end
|
86
|
+
|
87
|
+
memo << code << SEPARATOR
|
88
|
+
end.join("")
|
89
|
+
child = Child.new(self.class.bin, options)
|
90
|
+
pieces = child.out.split(SEPARATOR)
|
91
|
+
@multi ? pieces : pieces.first
|
92
|
+
end
|
93
|
+
|
94
|
+
alias_method :to_s, :colorize
|
95
|
+
|
96
|
+
SEPARATOR = "\000".freeze
|
97
|
+
end
|
98
|
+
end
|
data/test/albino_test.rb
CHANGED
@@ -1,5 +1,5 @@
|
|
1
|
-
require 'albino'
|
2
1
|
require 'rubygems'
|
2
|
+
require 'albino'
|
3
3
|
require 'test/unit'
|
4
4
|
require 'tempfile'
|
5
5
|
require 'mocha'
|
@@ -25,7 +25,8 @@ class AlbinoTest < Test::Unit::TestCase
|
|
25
25
|
|
26
26
|
def test_works_with_strings
|
27
27
|
syntaxer = Albino.new("class New\nend", :ruby)
|
28
|
-
assert_match %r(highlight), syntaxer.colorize
|
28
|
+
assert_match %r(highlight), code=syntaxer.colorize
|
29
|
+
assert_match %(<span class="nc">New</span>\n), code
|
29
30
|
end
|
30
31
|
|
31
32
|
def test_works_with_files
|
@@ -55,4 +56,4 @@ class AlbinoTest < Test::Unit::TestCase
|
|
55
56
|
@syntaxer.convert_options(:l => "'abc;'")
|
56
57
|
end
|
57
58
|
end
|
58
|
-
end
|
59
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'albino/multi'
|
3
|
+
require 'test/unit'
|
4
|
+
require 'tempfile'
|
5
|
+
require 'mocha'
|
6
|
+
|
7
|
+
class MultiTest < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
@syntaxer = Albino::Multi.new(File.new(__FILE__), :ruby)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_defaults_to_text
|
13
|
+
syntaxer = Albino::Multi.new('abc')
|
14
|
+
regex = /span/
|
15
|
+
assert_no_match regex, syntaxer.colorize
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_works_with_strings
|
19
|
+
syntaxer = Albino::Multi.new("class New\nend", :ruby)
|
20
|
+
assert_match %r(highlight), code=syntaxer.colorize
|
21
|
+
assert_match %(<span class="nc">New</span>\n), code
|
22
|
+
end
|
23
|
+
|
24
|
+
def test_works_with_multiple_code_fragments
|
25
|
+
syntaxer = Albino::Multi.new [
|
26
|
+
['ruby', "class New\nend"],
|
27
|
+
['python', "class New:\n pass"]]
|
28
|
+
codes = syntaxer.colorize
|
29
|
+
assert_equal 2, codes.size
|
30
|
+
assert_match %r(highlight), codes[0]
|
31
|
+
assert_match %r(highlight), codes[1]
|
32
|
+
assert_match %(<span class="nc">New</span>\n), codes[0]
|
33
|
+
assert_match %(<span class="p">:</span>), codes[1]
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_works_with_files
|
37
|
+
contents = "class New\nend"
|
38
|
+
syntaxer = Albino::Multi.new(contents, :ruby)
|
39
|
+
file_output = syntaxer.colorize
|
40
|
+
|
41
|
+
Tempfile.open 'albino-test' do |tmp|
|
42
|
+
tmp << contents
|
43
|
+
tmp.flush
|
44
|
+
syntaxer = Albino::Multi.new(File.new(tmp.path), :ruby)
|
45
|
+
assert_equal file_output, syntaxer.colorize
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_aliases_to_s
|
50
|
+
syntaxer = Albino::Multi.new(File.new(__FILE__), :ruby)
|
51
|
+
assert_equal @syntaxer.colorize, syntaxer.to_s
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_class_method_colorize
|
55
|
+
assert_equal @syntaxer.colorize, Albino::Multi.colorize(File.new(__FILE__), :ruby)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env python
|
2
|
+
|
3
|
+
# This script allows you to highlight multiple chunks of code with a single
|
4
|
+
# invocation. Expected input is on STDIN and takes the form of:
|
5
|
+
#
|
6
|
+
# <lexer>\000<code>\000<lexer>\000<code>...
|
7
|
+
#
|
8
|
+
# where <lexer> is the shortname of a Pygments lexer and <code> is the source
|
9
|
+
# code to be highlighted. Each lexer and code pair is separated with a NULL
|
10
|
+
# byte and pairs of lexer/code are also separated with NULL bytes.
|
11
|
+
#
|
12
|
+
# Output is a list of highlighted code blocks separated by NULL bytes in the
|
13
|
+
# same order in which they were received.
|
14
|
+
|
15
|
+
import sys, os, codecs
|
16
|
+
|
17
|
+
sys.stdout = codecs.getwriter('UTF-8')(sys.stdout)
|
18
|
+
|
19
|
+
vpath = os.path.realpath(__file__).split("/")
|
20
|
+
vpath.pop()
|
21
|
+
vpath.pop()
|
22
|
+
vpath = "/".join(vpath)
|
23
|
+
|
24
|
+
from pygments import highlight
|
25
|
+
from pygments.lexers import get_lexer_by_name
|
26
|
+
from pygments.formatters import HtmlFormatter
|
27
|
+
|
28
|
+
parts = sys.stdin.read().split("\000")
|
29
|
+
newparts = []
|
30
|
+
|
31
|
+
for i in range(len(parts) / 2):
|
32
|
+
lang = parts[i * 2]
|
33
|
+
code = parts[i * 2 + 1]
|
34
|
+
try:
|
35
|
+
lexer = get_lexer_by_name(lang)
|
36
|
+
except:
|
37
|
+
lexer = get_lexer_by_name('text')
|
38
|
+
newparts.append([code, lexer])
|
39
|
+
|
40
|
+
def hl(spec):
|
41
|
+
code = spec[0]
|
42
|
+
lexer = spec[1]
|
43
|
+
try:
|
44
|
+
return highlight(code, lexer, HtmlFormatter())
|
45
|
+
except:
|
46
|
+
lexer = get_lexer_by_name('text')
|
47
|
+
return highlight(code, lexer, HtmlFormatter())
|
48
|
+
|
49
|
+
for spec in newparts:
|
50
|
+
sys.stdout.write(hl(spec))
|
51
|
+
sys.stdout.write("\000")
|
52
|
+
|
metadata
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: albino
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
4
5
|
prerelease: false
|
5
6
|
segments:
|
6
7
|
- 1
|
7
|
-
- 2
|
8
8
|
- 3
|
9
|
-
|
9
|
+
- 0
|
10
|
+
version: 1.3.0
|
10
11
|
platform: ruby
|
11
12
|
authors:
|
12
13
|
- Chris Wanstrath
|
@@ -14,21 +15,37 @@ autorequire:
|
|
14
15
|
bindir: bin
|
15
16
|
cert_chain: []
|
16
17
|
|
17
|
-
date: 2011-
|
18
|
+
date: 2011-03-03 00:00:00 -08:00
|
18
19
|
default_executable:
|
19
20
|
dependencies:
|
20
21
|
- !ruby/object:Gem::Dependency
|
21
|
-
name:
|
22
|
+
name: posix-spawn
|
22
23
|
prerelease: false
|
23
24
|
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
24
26
|
requirements:
|
25
27
|
- - ">="
|
26
28
|
- !ruby/object:Gem::Version
|
29
|
+
hash: 3
|
27
30
|
segments:
|
28
31
|
- 0
|
29
32
|
version: "0"
|
30
|
-
type: :
|
33
|
+
type: :runtime
|
31
34
|
version_requirements: *id001
|
35
|
+
- !ruby/object:Gem::Dependency
|
36
|
+
name: mocha
|
37
|
+
prerelease: false
|
38
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
hash: 3
|
44
|
+
segments:
|
45
|
+
- 0
|
46
|
+
version: "0"
|
47
|
+
type: :development
|
48
|
+
version_requirements: *id002
|
32
49
|
description: Ruby wrapper for pygmentize.
|
33
50
|
email: chris@wanstrath.com
|
34
51
|
executables: []
|
@@ -43,9 +60,10 @@ files:
|
|
43
60
|
- Rakefile
|
44
61
|
- albino.gemspec
|
45
62
|
- lib/albino.rb
|
46
|
-
- lib/albino/
|
63
|
+
- lib/albino/multi.rb
|
47
64
|
- test/albino_test.rb
|
48
|
-
- test/
|
65
|
+
- test/multi_albino_test.rb
|
66
|
+
- vendor/multipygmentize
|
49
67
|
has_rdoc: true
|
50
68
|
homepage: http://github.com/github/albino
|
51
69
|
licenses: []
|
@@ -56,23 +74,27 @@ rdoc_options: []
|
|
56
74
|
require_paths:
|
57
75
|
- lib
|
58
76
|
required_ruby_version: !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
59
78
|
requirements:
|
60
79
|
- - ">="
|
61
80
|
- !ruby/object:Gem::Version
|
81
|
+
hash: 3
|
62
82
|
segments:
|
63
83
|
- 0
|
64
84
|
version: "0"
|
65
85
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
|
+
none: false
|
66
87
|
requirements:
|
67
88
|
- - ">="
|
68
89
|
- !ruby/object:Gem::Version
|
90
|
+
hash: 3
|
69
91
|
segments:
|
70
92
|
- 0
|
71
93
|
version: "0"
|
72
94
|
requirements: []
|
73
95
|
|
74
96
|
rubyforge_project: albino
|
75
|
-
rubygems_version: 1.3.
|
97
|
+
rubygems_version: 1.3.7
|
76
98
|
signing_key:
|
77
99
|
specification_version: 2
|
78
100
|
summary: Ruby wrapper for pygmentize.
|
data/lib/albino/process.rb
DELETED
@@ -1,294 +0,0 @@
|
|
1
|
-
class Albino
|
2
|
-
# Albino::Process includes logic for executing child processes and
|
3
|
-
# reading/writing from their standard input, output, and error streams.
|
4
|
-
#
|
5
|
-
# Create an run a process to completion:
|
6
|
-
#
|
7
|
-
# >> process = Albino::Process.new(['git', '--help'])
|
8
|
-
#
|
9
|
-
# Retrieve stdout or stderr output:
|
10
|
-
#
|
11
|
-
# >> process.out
|
12
|
-
# => "usage: git [--version] [--exec-path[=GIT_EXEC_PATH]]\n ..."
|
13
|
-
# >> process.err
|
14
|
-
# => ""
|
15
|
-
#
|
16
|
-
# Check process exit status information:
|
17
|
-
#
|
18
|
-
# >> process.status
|
19
|
-
# => #<Process::Status: pid=80718,exited(0)>
|
20
|
-
#
|
21
|
-
# Albino::Process is designed to take all input in a single string and
|
22
|
-
# provides all output as single strings. It is therefore not well suited
|
23
|
-
# to streaming large quantities of data in and out of commands.
|
24
|
-
#
|
25
|
-
# Q: Why not use popen3 or hand-roll fork/exec code?
|
26
|
-
#
|
27
|
-
# - It's more efficient than popen3 and provides meaningful process
|
28
|
-
# hierarchies because it performs a single fork/exec. (popen3 double forks
|
29
|
-
# to avoid needing to collect the exit status and also calls
|
30
|
-
# Process::detach which creates a Ruby Thread!!!!).
|
31
|
-
#
|
32
|
-
# - It's more portable than hand rolled pipe, fork, exec code because
|
33
|
-
# fork(2) and exec(2) aren't available on all platforms. In those cases,
|
34
|
-
# Albino::Process falls back to using whatever janky substitutes the platform
|
35
|
-
# provides.
|
36
|
-
#
|
37
|
-
# - It handles all max pipe buffer hang cases, which is non trivial to
|
38
|
-
# implement correctly and must be accounted for with either popen3 or
|
39
|
-
# hand rolled fork/exec code.
|
40
|
-
class Process
|
41
|
-
# Create and execute a new process.
|
42
|
-
#
|
43
|
-
# argv - Array of [command, arg1, ...] strings to use as the new
|
44
|
-
# process's argv. When argv is a String, the shell is used
|
45
|
-
# to interpret the command.
|
46
|
-
# env - The new process's environment variables. This is merged with
|
47
|
-
# the current environment as if by ENV.merge(env).
|
48
|
-
# options - Additional options:
|
49
|
-
# :input => str to write str to the process's stdin.
|
50
|
-
# :timeout => int number of seconds before we given up.
|
51
|
-
# :max => total number of output bytes
|
52
|
-
# A subset of Process:spawn options are also supported on all
|
53
|
-
# platforms:
|
54
|
-
# :chdir => str to start the process in different working dir.
|
55
|
-
#
|
56
|
-
# Returns a new Process instance that has already executed to completion.
|
57
|
-
# The out, err, and status attributes are immediately available.
|
58
|
-
def initialize(argv, env={}, options={})
|
59
|
-
@argv = argv
|
60
|
-
@env = env
|
61
|
-
|
62
|
-
@options = options.dup
|
63
|
-
@input = @options.delete(:input)
|
64
|
-
@timeout = @options.delete(:timeout)
|
65
|
-
@max = @options.delete(:max)
|
66
|
-
@options.delete(:chdir) if @options[:chdir].nil?
|
67
|
-
|
68
|
-
exec!
|
69
|
-
end
|
70
|
-
|
71
|
-
# All data written to the child process's stdout stream as a String.
|
72
|
-
attr_reader :out
|
73
|
-
|
74
|
-
# All data written to the child process's stderr stream as a String.
|
75
|
-
attr_reader :err
|
76
|
-
|
77
|
-
# A Process::Status object with information on how the child exited.
|
78
|
-
attr_reader :status
|
79
|
-
|
80
|
-
# Total command execution time (wall-clock time)
|
81
|
-
attr_reader :runtime
|
82
|
-
|
83
|
-
# Determine if the process did exit with a zero exit status.
|
84
|
-
def success?
|
85
|
-
@status && @status.success?
|
86
|
-
end
|
87
|
-
|
88
|
-
private
|
89
|
-
# Execute command, write input, and read output. This is called
|
90
|
-
# immediately when a new instance of this object is initialized.
|
91
|
-
def exec!
|
92
|
-
# when argv is a string, use /bin/sh to interpret command
|
93
|
-
argv = @argv
|
94
|
-
argv = ['/bin/sh', '-c', argv.to_str] if argv.respond_to?(:to_str)
|
95
|
-
|
96
|
-
# spawn the process and hook up the pipes
|
97
|
-
pid, stdin, stdout, stderr = popen4(@env, *(argv + [@options]))
|
98
|
-
|
99
|
-
# async read from all streams into buffers
|
100
|
-
@out, @err = read_and_write(@input, stdin, stdout, stderr, @timeout, @max)
|
101
|
-
|
102
|
-
# grab exit status
|
103
|
-
@status = waitpid(pid)
|
104
|
-
rescue Object => boom
|
105
|
-
[stdin, stdout, stderr].each { |fd| fd.close rescue nil }
|
106
|
-
if @status.nil?
|
107
|
-
::Process.kill('TERM', pid) rescue nil
|
108
|
-
@status = waitpid(pid) rescue nil
|
109
|
-
end
|
110
|
-
raise
|
111
|
-
end
|
112
|
-
|
113
|
-
# Exception raised when the total number of bytes output on the command's
|
114
|
-
# stderr and stdout streams exceeds the maximum output size (:max option).
|
115
|
-
class MaximumOutputExceeded < StandardError
|
116
|
-
end
|
117
|
-
|
118
|
-
# Exception raised when timeout is exceeded.
|
119
|
-
class TimeoutExceeded < StandardError
|
120
|
-
end
|
121
|
-
|
122
|
-
# Maximum buffer size for reading
|
123
|
-
BUFSIZE = (32 * 1024)
|
124
|
-
|
125
|
-
# Start a select loop writing any input on the child's stdin and reading
|
126
|
-
# any output from the child's stdout or stderr.
|
127
|
-
#
|
128
|
-
# input - String input to write on stdin. May be nil.
|
129
|
-
# stdin - The write side IO object for the child's stdin stream.
|
130
|
-
# stdout - The read side IO object for the child's stdout stream.
|
131
|
-
# stderr - The read side IO object for the child's stderr stream.
|
132
|
-
# timeout - An optional Numeric specifying the total number of seconds
|
133
|
-
# the read/write operations should occur for.
|
134
|
-
#
|
135
|
-
# Returns an [out, err] tuple where both elements are strings with all
|
136
|
-
# data written to the stdout and stderr streams, respectively.
|
137
|
-
# Raises TimeoutExceeded when all data has not been read / written within
|
138
|
-
# the duration specified in the timeout argument.
|
139
|
-
# Raises MaximumOutputExceeded when the total number of bytes output
|
140
|
-
# exceeds the amount specified by the max argument.
|
141
|
-
def read_and_write(input, stdin, stdout, stderr, timeout=nil, max=nil)
|
142
|
-
input ||= ''
|
143
|
-
max = nil if max && max <= 0
|
144
|
-
out, err = '', ''
|
145
|
-
offset = 0
|
146
|
-
|
147
|
-
timeout = nil if timeout && timeout <= 0.0
|
148
|
-
@runtime = 0.0
|
149
|
-
start = Time.now
|
150
|
-
|
151
|
-
writers = [stdin]
|
152
|
-
readers = [stdout, stderr]
|
153
|
-
t = timeout
|
154
|
-
while readers.any? || writers.any?
|
155
|
-
ready = IO.select(readers, writers, readers + writers, t)
|
156
|
-
raise TimeoutExceeded if ready.nil?
|
157
|
-
|
158
|
-
# write to stdin stream
|
159
|
-
ready[1].each do |fd|
|
160
|
-
begin
|
161
|
-
boom = nil
|
162
|
-
size = fd.write_nonblock(input)
|
163
|
-
input = input[size, input.size]
|
164
|
-
rescue Errno::EPIPE => boom
|
165
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
166
|
-
end
|
167
|
-
if boom || input.size == 0
|
168
|
-
stdin.close
|
169
|
-
writers.delete(stdin)
|
170
|
-
end
|
171
|
-
end
|
172
|
-
|
173
|
-
# read from stdout and stderr streams
|
174
|
-
ready[0].each do |fd|
|
175
|
-
buf = (fd == stdout) ? out : err
|
176
|
-
begin
|
177
|
-
buf << fd.readpartial(BUFSIZE)
|
178
|
-
rescue Errno::EAGAIN, Errno::EINTR
|
179
|
-
rescue EOFError
|
180
|
-
readers.delete(fd)
|
181
|
-
fd.close
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
# keep tabs on the total amount of time we've spent here
|
186
|
-
@runtime = Time.now - start
|
187
|
-
if timeout
|
188
|
-
t = timeout - @runtime
|
189
|
-
raise TimeoutExceeded if t < 0.0
|
190
|
-
end
|
191
|
-
|
192
|
-
# maybe we've hit our max output
|
193
|
-
if max && ready[0].any? && (out.size + err.size) > max
|
194
|
-
raise MaximumOutputExceeded
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
[out, err]
|
199
|
-
end
|
200
|
-
|
201
|
-
# Spawn a child process, perform IO redirection and environment prep, and
|
202
|
-
# return the running process's pid.
|
203
|
-
#
|
204
|
-
# This method implements a limited subset of Ruby 1.9's Process::spawn.
|
205
|
-
# The idea is that we can just use that when available, since most platforms
|
206
|
-
# will eventually build in special (and hopefully good) support for it.
|
207
|
-
#
|
208
|
-
# env - Hash of { name => val } environment variables set in the child
|
209
|
-
# process.
|
210
|
-
# argv - New process's argv as an Array. When this value is a string,
|
211
|
-
# the command may be run through the system /bin/sh or
|
212
|
-
# options - Supports a subset of Process::spawn options, including:
|
213
|
-
# :chdir => str to change the directory to str in the child
|
214
|
-
# FD => :close to close a file descriptor in the child
|
215
|
-
# :in => FD to redirect child's stdin to FD
|
216
|
-
# :out => FD to redirect child's stdout to FD
|
217
|
-
# :err => FD to redirect child's stderr to FD
|
218
|
-
#
|
219
|
-
# Returns the pid of the new process as an integer. The process exit status
|
220
|
-
# must be obtained using Process::waitpid.
|
221
|
-
def spawn(env, *argv)
|
222
|
-
options = (argv.pop if argv[-1].kind_of?(Hash)) || {}
|
223
|
-
fork do
|
224
|
-
# { fd => :close } in options means close that fd
|
225
|
-
options.each { |k,v| k.close if v == :close && !k.closed? }
|
226
|
-
|
227
|
-
# reopen stdin, stdout, and stderr on provided fds
|
228
|
-
STDIN.reopen(options[:in])
|
229
|
-
STDOUT.reopen(options[:out])
|
230
|
-
STDERR.reopen(options[:err])
|
231
|
-
|
232
|
-
# setup child environment
|
233
|
-
env.each { |k, v| ENV[k] = v }
|
234
|
-
|
235
|
-
# { :chdir => '/' } in options means change into that dir
|
236
|
-
::Dir.chdir(options[:chdir]) if options[:chdir]
|
237
|
-
|
238
|
-
# do the deed
|
239
|
-
::Kernel::exec(*argv)
|
240
|
-
exit! 1
|
241
|
-
end
|
242
|
-
end
|
243
|
-
|
244
|
-
# Start a process with spawn options and return
|
245
|
-
# popen4([env], command, arg1, arg2, [opt])
|
246
|
-
#
|
247
|
-
# env - The child process's environment as a Hash.
|
248
|
-
# command - The command and zero or more arguments.
|
249
|
-
# options - An options hash.
|
250
|
-
#
|
251
|
-
# See Ruby 1.9 IO.popen and Process::spawn docs for more info:
|
252
|
-
# http://www.ruby-doc.org/core-1.9/classes/IO.html#M001640
|
253
|
-
#
|
254
|
-
# Returns a [pid, stdin, stderr, stdout] tuple where pid is the child
|
255
|
-
# process's pid, stdin is a writeable IO object, and stdout + stderr are
|
256
|
-
# readable IO objects.
|
257
|
-
def popen4(*argv)
|
258
|
-
# create some pipes (see pipe(2) manual -- the ruby docs suck)
|
259
|
-
ird, iwr = IO.pipe
|
260
|
-
ord, owr = IO.pipe
|
261
|
-
erd, ewr = IO.pipe
|
262
|
-
|
263
|
-
# spawn the child process with either end of pipes hooked together
|
264
|
-
opts =
|
265
|
-
((argv.pop if argv[-1].is_a?(Hash)) || {}).merge(
|
266
|
-
# redirect fds # close other sides
|
267
|
-
:in => ird, iwr => :close,
|
268
|
-
:out => owr, ord => :close,
|
269
|
-
:err => ewr, erd => :close
|
270
|
-
)
|
271
|
-
pid = spawn(*(argv + [opts]))
|
272
|
-
|
273
|
-
[pid, iwr, ord, erd]
|
274
|
-
ensure
|
275
|
-
# we're in the parent, close child-side fds
|
276
|
-
[ird, owr, ewr].each { |fd| fd.close }
|
277
|
-
end
|
278
|
-
|
279
|
-
# Wait for the child process to exit
|
280
|
-
#
|
281
|
-
# Returns the Process::Status object obtained by reaping the process.
|
282
|
-
def waitpid(pid)
|
283
|
-
::Process::waitpid(pid)
|
284
|
-
$?
|
285
|
-
end
|
286
|
-
|
287
|
-
# Use native Process::spawn implementation on Ruby 1.9.
|
288
|
-
if ::Process.respond_to?(:spawn)
|
289
|
-
def spawn(*argv)
|
290
|
-
::Process.spawn(*argv)
|
291
|
-
end
|
292
|
-
end
|
293
|
-
end
|
294
|
-
end
|
data/test/process_test.rb
DELETED
@@ -1,102 +0,0 @@
|
|
1
|
-
require 'albino'
|
2
|
-
require 'rubygems'
|
3
|
-
require 'test/unit'
|
4
|
-
|
5
|
-
class TestProcess < Test::Unit::TestCase
|
6
|
-
def test_argv_array_execs
|
7
|
-
p = Albino::Process.new(['printf', '%s %s %s', '1', '2', '3 4'])
|
8
|
-
assert p.success?
|
9
|
-
assert_equal "1 2 3 4", p.out
|
10
|
-
end
|
11
|
-
|
12
|
-
def test_argv_string_uses_sh
|
13
|
-
p = Albino::Process.new("echo via /bin/sh")
|
14
|
-
assert p.success?
|
15
|
-
assert_equal "via /bin/sh\n", p.out
|
16
|
-
end
|
17
|
-
|
18
|
-
def test_stdout
|
19
|
-
p = Albino::Process.new(['echo', 'boom'])
|
20
|
-
assert_equal "boom\n", p.out
|
21
|
-
assert_equal "", p.err
|
22
|
-
end
|
23
|
-
|
24
|
-
def test_stderr
|
25
|
-
p = Albino::Process.new('echo boom 1>&2')
|
26
|
-
assert_equal "", p.out
|
27
|
-
assert_equal "boom\n", p.err
|
28
|
-
end
|
29
|
-
|
30
|
-
def test_status
|
31
|
-
p = Albino::Process.new('exit 3')
|
32
|
-
assert !p.status.success?
|
33
|
-
assert_equal 3, p.status.exitstatus
|
34
|
-
end
|
35
|
-
|
36
|
-
def test_env
|
37
|
-
p = Albino::Process.new('echo $FOO', { 'FOO' => 'BOOYAH' })
|
38
|
-
assert_equal "BOOYAH\n", p.out
|
39
|
-
end
|
40
|
-
|
41
|
-
def test_chdir
|
42
|
-
p = Albino::Process.new(["pwd"], {}, :chdir => File.dirname(Dir.pwd))
|
43
|
-
assert_equal File.dirname(Dir.pwd) + "\n", p.out
|
44
|
-
end
|
45
|
-
|
46
|
-
def test_input
|
47
|
-
input = "HEY NOW\n" * 100_000 # 800K
|
48
|
-
p = Albino::Process.new(['wc', '-l'], {}, :input => input)
|
49
|
-
assert_equal 100_000, p.out.strip.to_i
|
50
|
-
end
|
51
|
-
|
52
|
-
def test_max
|
53
|
-
assert_raise Albino::Process::MaximumOutputExceeded do
|
54
|
-
Albino::Process.new(['yes'], {}, :max => 100_000)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
def test_max_with_child_hierarchy
|
59
|
-
assert_raise Albino::Process::MaximumOutputExceeded do
|
60
|
-
Albino::Process.new(['/bin/sh', '-c', 'yes'], {}, :max => 100_000)
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
def test_max_with_stubborn_child
|
65
|
-
assert_raise Albino::Process::MaximumOutputExceeded do
|
66
|
-
Albino::Process.new("trap '' TERM; yes", {}, :max => 100_000)
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def test_timeout
|
71
|
-
assert_raise Albino::Process::TimeoutExceeded do
|
72
|
-
Albino::Process.new(['sleep 1'], {}, :timeout => 0.05)
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
def test_timeout_with_child_hierarchy
|
77
|
-
assert_raise Albino::Process::TimeoutExceeded do
|
78
|
-
Albino::Process.new(['/bin/sh', '-c', 'yes'], {}, :timeout => 0.05)
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
def test_lots_of_input_and_lots_of_output_at_the_same_time
|
83
|
-
input = "stuff on stdin \n" * 1_000
|
84
|
-
command = "
|
85
|
-
while read line
|
86
|
-
do
|
87
|
-
echo stuff on stdout;
|
88
|
-
echo stuff on stderr 1>&2;
|
89
|
-
done
|
90
|
-
"
|
91
|
-
p = Albino::Process.new(['/bin/sh', '-c', command], {}, :input => input)
|
92
|
-
assert_equal input.size, p.out.size
|
93
|
-
assert_equal input.size, p.err.size
|
94
|
-
assert p.success?
|
95
|
-
end
|
96
|
-
|
97
|
-
def test_input_cannot_be_written_due_to_broken_pipe
|
98
|
-
input = "1" * 100_000
|
99
|
-
p = Albino::Process.new(['false'], {}, :input => input)
|
100
|
-
assert !p.success?
|
101
|
-
end
|
102
|
-
end
|