tty-command 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,15 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty/command/printers/abstract'
4
+
5
+ module TTY
6
+ class Command
7
+ module Printers
8
+ class Null < Abstract
9
+ def write(*)
10
+ # Do nothing
11
+ end
12
+ end # Null
13
+ end # Printers
14
+ end # Command
15
+ 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