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.
@@ -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