tty-command 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +3 -0
- data/.travis.yml +25 -0
- data/CHANGELOG.md +7 -0
- data/CODE_OF_CONDUCT.md +49 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +392 -0
- data/Rakefile +10 -0
- data/bin/console +6 -0
- data/bin/setup +6 -0
- data/examples/bash.rb +12 -0
- data/examples/basic.rb +9 -0
- data/examples/env.rb +9 -0
- data/examples/logger.rb +10 -0
- data/examples/redirect_stderr.rb +10 -0
- data/examples/redirect_stdout.rb +7 -0
- data/examples/timeout.rb +7 -0
- data/lib/tty-command.rb +1 -0
- data/lib/tty/command.rb +160 -0
- data/lib/tty/command/cmd.rb +153 -0
- data/lib/tty/command/dry_runner.rb +19 -0
- data/lib/tty/command/execute.rb +134 -0
- data/lib/tty/command/exit_error.rb +28 -0
- data/lib/tty/command/printers/abstract.rb +50 -0
- data/lib/tty/command/printers/null.rb +15 -0
- data/lib/tty/command/printers/pretty.rb +67 -0
- data/lib/tty/command/printers/progress.rb +31 -0
- data/lib/tty/command/printers/quiet.rb +25 -0
- data/lib/tty/command/process_runner.rb +101 -0
- data/lib/tty/command/result.rb +72 -0
- data/lib/tty/command/truncator.rb +109 -0
- data/lib/tty/command/version.rb +7 -0
- data/tasks/console.rake +10 -0
- data/tasks/coverage.rake +11 -0
- data/tasks/spec.rake +29 -0
- data/tty-command.gemspec +27 -0
- metadata +146 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Command
|
5
|
+
# An ExitError reports an unsuccessful exit by command.
|
6
|
+
#
|
7
|
+
# The error message includes:
|
8
|
+
# * the name of command executed
|
9
|
+
# * the exit status
|
10
|
+
# * stdout bytes
|
11
|
+
# * stderr bytes
|
12
|
+
#
|
13
|
+
# @api private
|
14
|
+
class ExitError < RuntimeError
|
15
|
+
def initialize(cmd_name, result)
|
16
|
+
super(info(cmd_name, result))
|
17
|
+
end
|
18
|
+
|
19
|
+
def info(cmd_name, result)
|
20
|
+
message = ''
|
21
|
+
message << "Running `#{cmd_name}` failed with\n"
|
22
|
+
message << " exit status: #{result.exit_status}\n"
|
23
|
+
message << " stdout: #{result.out.strip.empty? ? 'Nothing written' : result.out.strip}\n"
|
24
|
+
message << " stderr: #{result.err.strip.empty? ? 'Nothing written' : result.err.strip}\n"
|
25
|
+
end
|
26
|
+
end # ExitError
|
27
|
+
end # Command
|
28
|
+
end # TTY
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Command
|
7
|
+
module Printers
|
8
|
+
class Abstract
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
def_delegators :@color, :decorate
|
12
|
+
|
13
|
+
attr_reader :output, :options
|
14
|
+
|
15
|
+
# Initialize a Printer object
|
16
|
+
#
|
17
|
+
# @param [IO] output
|
18
|
+
# the printer output
|
19
|
+
#
|
20
|
+
# @api public
|
21
|
+
def initialize(output, options = {})
|
22
|
+
@output = output
|
23
|
+
@options = options
|
24
|
+
enabled = options.fetch(:color) { true }
|
25
|
+
@color = ::Pastel.new(output: output, enabled: enabled)
|
26
|
+
end
|
27
|
+
|
28
|
+
def print_command_start(cmd, *args)
|
29
|
+
write(cmd.to_command + "#{args.join}")
|
30
|
+
end
|
31
|
+
|
32
|
+
def print_command_out_data(cmd, *args)
|
33
|
+
write(args.join)
|
34
|
+
end
|
35
|
+
|
36
|
+
def print_command_err_data(cmd, *args)
|
37
|
+
write(args.join)
|
38
|
+
end
|
39
|
+
|
40
|
+
def print_command_exit(cmd, *args)
|
41
|
+
write(args.join)
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(message)
|
45
|
+
raise NotImplemented, "Abstract printer cannot be used"
|
46
|
+
end
|
47
|
+
end # Abstract
|
48
|
+
end # Printers
|
49
|
+
end # Command
|
50
|
+
end # TTY
|
@@ -0,0 +1,67 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
require 'tty/command/printers/abstract'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Command
|
8
|
+
module Printers
|
9
|
+
class Pretty < Abstract
|
10
|
+
def print_command_start(cmd, *args)
|
11
|
+
message = "Running #{decorate(cmd.to_command, :yellow, :bold)}"
|
12
|
+
message << args.map(&:chomp).join(' ') unless args.empty?
|
13
|
+
write(message, cmd.uuid)
|
14
|
+
end
|
15
|
+
|
16
|
+
def print_command_out_data(cmd, *args)
|
17
|
+
message = args.map(&:chomp).join(' ')
|
18
|
+
write("\t#{message}", cmd.uuid)
|
19
|
+
end
|
20
|
+
|
21
|
+
def print_command_err_data(cmd, *args)
|
22
|
+
message = args.map(&:chomp).join(' ')
|
23
|
+
write("\t" + decorate(message, :red), cmd.uuid)
|
24
|
+
end
|
25
|
+
|
26
|
+
def print_command_exit(cmd, status, runtime, *args)
|
27
|
+
runtime = "%5.3f %s" % [runtime, pluralize(runtime, 'second')]
|
28
|
+
message = "Finished in #{runtime}"
|
29
|
+
message << " with exit status #{status}" if status
|
30
|
+
message << " (#{success_or_failure(status)})"
|
31
|
+
write(message, cmd.uuid)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Write message out to output
|
35
|
+
#
|
36
|
+
# @api private
|
37
|
+
def write(message, uuid = nil)
|
38
|
+
uuid_needed = options.fetch(:uuid) { true }
|
39
|
+
out = ''
|
40
|
+
if uuid_needed
|
41
|
+
out << "[#{decorate(uuid, :green)}] " unless uuid.nil?
|
42
|
+
end
|
43
|
+
out << "#{message}\n"
|
44
|
+
output << out
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
# Pluralize word based on a count
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
def pluralize(count, word)
|
53
|
+
"#{word}#{'s' unless count.to_f == 1}"
|
54
|
+
end
|
55
|
+
|
56
|
+
# @api private
|
57
|
+
def success_or_failure(status)
|
58
|
+
if status == 0
|
59
|
+
decorate('successful', :green, :bold)
|
60
|
+
else
|
61
|
+
decorate('failed', :red, :bold)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end # Pretty
|
65
|
+
end # Printers
|
66
|
+
end # Command
|
67
|
+
end # TTY
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'pastel'
|
4
|
+
require 'tty/command/printers/abstract'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Command
|
8
|
+
module Printers
|
9
|
+
class Progress < Abstract
|
10
|
+
|
11
|
+
def print_command_exit(cmd, status, runtime, *args)
|
12
|
+
output.print(success_or_failure(status))
|
13
|
+
end
|
14
|
+
|
15
|
+
def write(*)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
# @api private
|
21
|
+
def success_or_failure(status)
|
22
|
+
if status == 0
|
23
|
+
decorate('.', :green)
|
24
|
+
else
|
25
|
+
decorate('F', :red)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end # Progress
|
29
|
+
end # Printers
|
30
|
+
end # Command
|
31
|
+
end # TTY
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty/command/printers/abstract'
|
4
|
+
|
5
|
+
module TTY
|
6
|
+
class Command
|
7
|
+
module Printers
|
8
|
+
class Quiet < Abstract
|
9
|
+
attr_reader :output, :options
|
10
|
+
|
11
|
+
def print_command_start(cmd)
|
12
|
+
# quiet
|
13
|
+
end
|
14
|
+
|
15
|
+
def print_command_exit(cmd, *args)
|
16
|
+
# quiet
|
17
|
+
end
|
18
|
+
|
19
|
+
def write(message)
|
20
|
+
output.print(message)
|
21
|
+
end
|
22
|
+
end # Progress
|
23
|
+
end # Printers
|
24
|
+
end # Command
|
25
|
+
end # TTY
|
@@ -0,0 +1,101 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'tty/command/execute'
|
4
|
+
require 'tty/command/result'
|
5
|
+
require 'tty/command/truncator'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Command
|
9
|
+
class ProcessRunner
|
10
|
+
include Execute
|
11
|
+
|
12
|
+
# Initialize a Runner object
|
13
|
+
#
|
14
|
+
# @param [Printer] printer
|
15
|
+
# the printer to use for logging
|
16
|
+
#
|
17
|
+
# @api private
|
18
|
+
def initialize(printer)
|
19
|
+
@printer = printer
|
20
|
+
end
|
21
|
+
|
22
|
+
# Execute child process
|
23
|
+
# @api public
|
24
|
+
def run(cmd)
|
25
|
+
timeout = cmd.options[:timeout]
|
26
|
+
@printer.print_command_start(cmd)
|
27
|
+
start = Time.now
|
28
|
+
|
29
|
+
spawn(cmd) do |pid, stdin, stdout, stderr|
|
30
|
+
stdout_data, stderr_data = read_streams(cmd, stdout, stderr)
|
31
|
+
|
32
|
+
runtime = Time.now - start
|
33
|
+
handle_timeout(timeout, runtime, pid)
|
34
|
+
status = waitpid(pid)
|
35
|
+
|
36
|
+
@printer.print_command_exit(cmd, status, runtime)
|
37
|
+
|
38
|
+
Result.new(status, stdout_data, stderr_data)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# @api private
|
45
|
+
def handle_timeout(timeout, runtime, pid)
|
46
|
+
return unless timeout
|
47
|
+
|
48
|
+
t = timeout - runtime
|
49
|
+
if t < 0.0
|
50
|
+
::Process.kill(:KILL, pid)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# @api private
|
55
|
+
def read_streams(cmd, stdout, stderr)
|
56
|
+
stdout_data = ''
|
57
|
+
stderr_data = Truncator.new
|
58
|
+
timeout = cmd.options[:timeout]
|
59
|
+
|
60
|
+
stdout_thread = Thread.new do
|
61
|
+
begin
|
62
|
+
while (line = stdout.gets)
|
63
|
+
stdout_data << line
|
64
|
+
@printer.print_command_out_data(cmd, line)
|
65
|
+
end
|
66
|
+
rescue TimeoutExceeded
|
67
|
+
stdout.close
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
stderr_thread = Thread.new do
|
72
|
+
begin
|
73
|
+
while (line = stderr.gets)
|
74
|
+
stderr_data << line
|
75
|
+
@printer.print_command_err_data(cmd, line)
|
76
|
+
end
|
77
|
+
rescue TimeoutExceeded
|
78
|
+
stderr.close
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
[stdout_thread, stderr_thread].each do |th|
|
83
|
+
result = th.join(timeout)
|
84
|
+
if result.nil?
|
85
|
+
stdout_thread.raise(TimeoutExceeded)
|
86
|
+
stderr_thread.raise(TimeoutExceeded)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
[stdout_data, stderr_data.read]
|
90
|
+
end
|
91
|
+
|
92
|
+
# @api private
|
93
|
+
def waitpid(pid)
|
94
|
+
::Process.waitpid(pid, Process::WUNTRACED)
|
95
|
+
$?.exitstatus
|
96
|
+
rescue Errno::ECHILD
|
97
|
+
# In JRuby, waiting on a finished pid raises.
|
98
|
+
end
|
99
|
+
end # ProcessRunner
|
100
|
+
end # Command
|
101
|
+
end # TTY
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Command
|
5
|
+
# Encapsulates the information on the command executed
|
6
|
+
#
|
7
|
+
# @api public
|
8
|
+
class Result
|
9
|
+
def initialize(status, out, err)
|
10
|
+
@status = status
|
11
|
+
@out = out
|
12
|
+
@err = err
|
13
|
+
end
|
14
|
+
|
15
|
+
# All data written out to process's stdout stream
|
16
|
+
def out
|
17
|
+
@out
|
18
|
+
end
|
19
|
+
alias_method :stdout, :out
|
20
|
+
|
21
|
+
# All data written out to process's stdin stream
|
22
|
+
def err
|
23
|
+
@err
|
24
|
+
end
|
25
|
+
alias_method :stderr, :err
|
26
|
+
|
27
|
+
# Information on how the process exited
|
28
|
+
#
|
29
|
+
# @api public
|
30
|
+
def exit_status
|
31
|
+
@status
|
32
|
+
end
|
33
|
+
alias_method :exitstatus, :exit_status
|
34
|
+
alias_method :status, :exit_status
|
35
|
+
|
36
|
+
def to_i
|
37
|
+
@status
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@status.to_s
|
42
|
+
end
|
43
|
+
|
44
|
+
def to_ary
|
45
|
+
[@out, @err]
|
46
|
+
end
|
47
|
+
|
48
|
+
def exited?
|
49
|
+
@status != nil
|
50
|
+
end
|
51
|
+
alias_method :complete?, :exited?
|
52
|
+
|
53
|
+
def success?
|
54
|
+
if exited?
|
55
|
+
@status == 0
|
56
|
+
else
|
57
|
+
false
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def failure?
|
62
|
+
!success?
|
63
|
+
end
|
64
|
+
alias_method :failed?, :failure?
|
65
|
+
|
66
|
+
def ==(other)
|
67
|
+
return false unless other.is_a?(TTY::Command::Result)
|
68
|
+
@status == other.to_i && to_ary == other.to_ary
|
69
|
+
end
|
70
|
+
end # Result
|
71
|
+
end # Command
|
72
|
+
end # TTY
|
@@ -0,0 +1,109 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Command
|
5
|
+
# Retains the first N bytes and the last N bytes from written content
|
6
|
+
#
|
7
|
+
# @api private
|
8
|
+
class Truncator
|
9
|
+
# Default maximum byte size for prefix & suffix
|
10
|
+
DEFAULT_SIZE = 32 << 10
|
11
|
+
|
12
|
+
# Create a Truncator
|
13
|
+
#
|
14
|
+
# @param [Hash] options
|
15
|
+
# @option options [Number] max_size
|
16
|
+
#
|
17
|
+
# @api public
|
18
|
+
def initialize(options = {})
|
19
|
+
@max_size = options.fetch(:max_size) { DEFAULT_SIZE }
|
20
|
+
@prefix = ''
|
21
|
+
@suffix = ''
|
22
|
+
@skipped = 0
|
23
|
+
end
|
24
|
+
|
25
|
+
# Write content
|
26
|
+
#
|
27
|
+
# @param [String] content
|
28
|
+
# the content to write
|
29
|
+
#
|
30
|
+
# @return [nil]
|
31
|
+
#
|
32
|
+
# @api public
|
33
|
+
def write(content)
|
34
|
+
content = content.to_s.dup
|
35
|
+
|
36
|
+
content = append(content, @prefix)
|
37
|
+
|
38
|
+
if (over = (content.bytesize - @max_size)) > 0
|
39
|
+
content = content[over..-1]
|
40
|
+
@skipped += over
|
41
|
+
end
|
42
|
+
|
43
|
+
content = append(content, @suffix)
|
44
|
+
|
45
|
+
# suffix is full but we still have content to write
|
46
|
+
while content.bytesize > 0
|
47
|
+
content = copy(content, @suffix)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
alias_method :<<, :write
|
51
|
+
|
52
|
+
# Truncated representation of the content
|
53
|
+
#
|
54
|
+
# @return [String]
|
55
|
+
#
|
56
|
+
# @api public
|
57
|
+
def read
|
58
|
+
return @prefix if @suffix.empty?
|
59
|
+
|
60
|
+
if @skipped.zero?
|
61
|
+
return @prefix << @suffix
|
62
|
+
end
|
63
|
+
|
64
|
+
res = ''
|
65
|
+
res << @prefix
|
66
|
+
res << "\n... omitting #{@skipped} bytes ...\n"
|
67
|
+
res << @suffix
|
68
|
+
res
|
69
|
+
end
|
70
|
+
alias_method :to_s, :read
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
# Copy minimum bytes from source to destination
|
75
|
+
#
|
76
|
+
# @return [String]
|
77
|
+
# the remaining content
|
78
|
+
#
|
79
|
+
# @api private
|
80
|
+
def copy(value, dest)
|
81
|
+
bytes = value.bytesize
|
82
|
+
n = bytes < dest.bytesize ? bytes : dest.bytesize
|
83
|
+
|
84
|
+
head, tail = dest[0...n], dest[n..-1]
|
85
|
+
dest.replace("#{tail}#{value[0...n]}")
|
86
|
+
@skipped += head.bytesize
|
87
|
+
value[n..-1]
|
88
|
+
end
|
89
|
+
|
90
|
+
# Append value to destination
|
91
|
+
#
|
92
|
+
# @param [String] value
|
93
|
+
#
|
94
|
+
# @param [String] dst
|
95
|
+
#
|
96
|
+
# @api private
|
97
|
+
def append(value, dst)
|
98
|
+
remain = @max_size - dst.bytesize
|
99
|
+
if remain > 0
|
100
|
+
value_bytes = value.to_s.bytesize
|
101
|
+
offset = value_bytes < remain ? value_bytes : remain
|
102
|
+
dst << value[0...offset]
|
103
|
+
value = value[offset..-1]
|
104
|
+
end
|
105
|
+
value
|
106
|
+
end
|
107
|
+
end # Truncator
|
108
|
+
end # Command
|
109
|
+
end # TTY
|