tty-command 0.7.0 → 0.10.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.
Files changed (48) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -0
  3. data/LICENSE.txt +1 -1
  4. data/README.md +134 -64
  5. data/lib/tty-command.rb +1 -3
  6. data/lib/tty/command.rb +19 -18
  7. data/lib/tty/command/child_process.rb +23 -16
  8. data/lib/tty/command/cmd.rb +16 -12
  9. data/lib/tty/command/dry_runner.rb +4 -5
  10. data/lib/tty/command/exit_error.rb +1 -2
  11. data/lib/tty/command/printers/abstract.rb +11 -9
  12. data/lib/tty/command/printers/null.rb +1 -4
  13. data/lib/tty/command/printers/pretty.rb +29 -22
  14. data/lib/tty/command/printers/progress.rb +3 -8
  15. data/lib/tty/command/printers/quiet.rb +18 -8
  16. data/lib/tty/command/process_runner.rb +90 -67
  17. data/lib/tty/command/result.rb +0 -1
  18. data/lib/tty/command/truncator.rb +3 -4
  19. data/lib/tty/command/version.rb +2 -2
  20. metadata +24 -64
  21. data/.gitignore +0 -9
  22. data/.rspec +0 -4
  23. data/.travis.yml +0 -28
  24. data/CODE_OF_CONDUCT.md +0 -49
  25. data/Gemfile +0 -14
  26. data/Rakefile +0 -10
  27. data/appveyor.yml +0 -24
  28. data/benchmarks/memory.rb +0 -11
  29. data/bin/console +0 -6
  30. data/bin/setup +0 -6
  31. data/examples/bash.rb +0 -12
  32. data/examples/basic.rb +0 -9
  33. data/examples/cli +0 -4
  34. data/examples/env.rb +0 -9
  35. data/examples/logger.rb +0 -10
  36. data/examples/output.rb +0 -10
  37. data/examples/pty.rb +0 -7
  38. data/examples/redirect_stderr.rb +0 -10
  39. data/examples/redirect_stdin.rb +0 -16
  40. data/examples/redirect_stdout.rb +0 -10
  41. data/examples/stdin_input.rb +0 -10
  42. data/examples/threaded.rb +0 -12
  43. data/examples/timeout.rb +0 -7
  44. data/examples/wait.rb +0 -21
  45. data/tasks/console.rake +0 -11
  46. data/tasks/coverage.rake +0 -11
  47. data/tasks/spec.rake +0 -29
  48. data/tty-command.gemspec +0 -27
@@ -1,14 +1,9 @@
1
- # encoding: utf-8
2
- # frozen_string_literal: true
3
-
4
- require 'pastel'
5
- require_relative 'abstract'
1
+ require_relative "abstract"
6
2
 
7
3
  module TTY
8
4
  class Command
9
5
  module Printers
10
6
  class Progress < Abstract
11
-
12
7
  def print_command_exit(cmd, status, runtime, *args)
13
8
  output.print(success_or_failure(status))
14
9
  end
@@ -21,9 +16,9 @@ module TTY
21
16
  # @api private
22
17
  def success_or_failure(status)
23
18
  if status == 0
24
- decorate('.', :green)
19
+ decorate(".", :green)
25
20
  else
26
- decorate('F', :red)
21
+ decorate("F", :red)
27
22
  end
28
23
  end
29
24
  end # Progress
@@ -1,23 +1,33 @@
1
- # encoding: utf-8
2
-
3
- require_relative 'abstract'
1
+ require_relative "abstract"
4
2
 
5
3
  module TTY
6
4
  class Command
7
5
  module Printers
8
6
  class Quiet < Abstract
9
- attr_reader :output, :options
10
-
11
7
  def print_command_start(cmd)
12
8
  # quiet
13
9
  end
14
10
 
15
- def print_command_exit(cmd, *args)
11
+ def print_command_out_data(cmd, *args)
12
+ write(cmd, args.join(" "), out_data)
13
+ end
14
+
15
+ def print_command_err_data(cmd, *args)
16
+ write(cmd, args.join(" "), err_data)
17
+ end
18
+
19
+ def print_command_exit(cmd, status, *args)
20
+ unless !cmd.only_output_on_error || status.zero?
21
+ output << out_data
22
+ output << err_data
23
+ end
24
+
16
25
  # quiet
17
26
  end
18
27
 
19
- def write(message)
20
- output << message
28
+ def write(cmd, message, data = nil)
29
+ target = (cmd.only_output_on_error && !data.nil?) ? data : output
30
+ target << message
21
31
  end
22
32
  end # Progress
23
33
  end # Printers
@@ -1,11 +1,10 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
- require 'thread'
3
+ require "thread"
5
4
 
6
- require_relative 'child_process'
7
- require_relative 'result'
8
- require_relative 'truncator'
5
+ require_relative "child_process"
6
+ require_relative "result"
7
+ require_relative "truncator"
9
8
 
10
9
  module TTY
11
10
  class Command
@@ -23,7 +22,8 @@ module TTY
23
22
  @cmd = cmd
24
23
  @timeout = cmd.options[:timeout]
25
24
  @input = cmd.options[:input]
26
- @signal = cmd.options[:signal] || :TERM
25
+ @signal = cmd.options[:signal] || "SIGKILL"
26
+ @binmode = cmd.options[:binmode]
27
27
  @printer = printer
28
28
  @block = block
29
29
  end
@@ -40,22 +40,10 @@ module TTY
40
40
  def run!
41
41
  @printer.print_command_start(cmd)
42
42
  start = Time.now
43
- runtime = 0.0
44
43
 
45
44
  pid, stdin, stdout, stderr = ChildProcess.spawn(cmd)
46
45
 
47
- # no input to write, close child's stdin pipe
48
- stdin.close if (@input.nil? || @input.empty?) && !stdin.nil?
49
-
50
- readers = [stdout, stderr]
51
- writers = [@input && stdin].compact
52
-
53
- while writers.any?
54
- ready_readers, ready_writers = IO.select(readers, writers, [], @timeout)
55
- raise TimeoutExceeded if ready_readers.nil? || ready_writers.nil?
56
-
57
- write_stream(ready_writers, writers)
58
- end
46
+ write_stream(stdin, @input)
59
47
 
60
48
  stdout_data, stderr_data = read_streams(stdout, stderr)
61
49
 
@@ -67,6 +55,10 @@ module TTY
67
55
  Result.new(status, stdout_data, stderr_data, runtime)
68
56
  ensure
69
57
  [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
58
+ if pid # Ensure no zombie processes
59
+ ::Process.detach(pid)
60
+ terminate(pid)
61
+ end
70
62
  end
71
63
 
72
64
  # Stop a process marked by pid
@@ -80,6 +72,9 @@ module TTY
80
72
 
81
73
  private
82
74
 
75
+ # The buffer size for reading stdout and stderr
76
+ BUFSIZE = 16 * 1024
77
+
83
78
  # @api private
84
79
  def handle_timeout(runtime)
85
80
  return unless @timeout
@@ -91,28 +86,35 @@ module TTY
91
86
  # Write the input to the process stdin
92
87
  #
93
88
  # @api private
94
- def write_stream(ready_writers, writers)
89
+ def write_stream(stream, input)
95
90
  start = Time.now
96
- ready_writers.each do |fd|
97
- begin
98
- err = nil
99
- size = fd.write(@input)
100
- @input = @input.byteslice(size..-1)
101
- rescue IO::WaitWritable
102
- rescue Errno::EPIPE => err
103
- # The pipe closed before all input written
104
- # Probably process exited prematurely
105
- fd.close
106
- writers.delete(fd)
107
- end
108
- if err || @input.bytesize == 0
109
- fd.close
110
- writers.delete(fd)
111
- end
91
+ writers = [input && stream].compact
92
+
93
+ while writers.any?
94
+ ready = IO.select(nil, writers, writers, @timeout)
95
+ raise TimeoutExceeded if ready.nil?
96
+
97
+ ready[1].each do |writer|
98
+ begin
99
+ err = nil
100
+ size = writer.write(@input)
101
+ input = input.byteslice(size..-1)
102
+ rescue IO::WaitWritable
103
+ rescue Errno::EPIPE => err
104
+ # The pipe closed before all input written
105
+ # Probably process exited prematurely
106
+ writer.close
107
+ writers.delete(writer)
108
+ end
109
+ if err || input.bytesize == 0
110
+ writer.close
111
+ writers.delete(writer)
112
+ end
112
113
 
113
- # control total time spent writing
114
- runtime = Time.now - start
115
- handle_timeout(runtime)
114
+ # control total time spent writing
115
+ runtime = Time.now - start
116
+ handle_timeout(runtime)
117
+ end
116
118
  end
117
119
  end
118
120
 
@@ -126,53 +128,74 @@ module TTY
126
128
  stdout_data = []
127
129
  stderr_data = Truncator.new
128
130
 
129
- out_buffer = -> (line) {
130
- stdout_data << line
131
- @printer.print_command_out_data(cmd, line)
132
- @block.(line, nil) if @block
131
+ out_handler = ->(data) {
132
+ stdout_data << data
133
+ @printer.print_command_out_data(cmd, data)
134
+ @block.(data, nil) if @block
133
135
  }
134
136
 
135
- err_buffer = -> (line) {
136
- stderr_data << line
137
- @printer.print_command_err_data(cmd, line)
138
- @block.(nil, line) if @block
137
+ err_handler = ->(data) {
138
+ stderr_data << data
139
+ @printer.print_command_err_data(cmd, data)
140
+ @block.(nil, data) if @block
139
141
  }
140
142
 
141
- stdout_thread = read_stream(stdout, out_buffer)
142
- stderr_thread = read_stream(stderr, err_buffer)
143
+ stdout_thread = read_stream(stdout, out_handler)
144
+ stderr_thread = read_stream(stderr, err_handler)
143
145
 
144
146
  stdout_thread.join
145
147
  stderr_thread.join
146
148
 
147
- [stdout_data.join, stderr_data.read]
149
+ encoding = @binmode ? Encoding::BINARY : Encoding::UTF_8
150
+
151
+ [
152
+ stdout_data.join.force_encoding(encoding),
153
+ stderr_data.read.dup.force_encoding(encoding)
154
+ ]
148
155
  end
149
156
 
150
- def read_stream(stream, buffer)
157
+ # Read stream and invoke handler when data becomes available
158
+ #
159
+ # @param [IO] stream
160
+ # the stream to read data from
161
+ # @param [Proc] handler
162
+ # the handler to call when data becomes available
163
+ #
164
+ # @api private
165
+ def read_stream(stream, handler)
151
166
  Thread.new do
167
+ if Thread.current.respond_to?(:report_on_exception)
168
+ Thread.current.report_on_exception = false
169
+ end
152
170
  Thread.current[:cmd_start] = Time.now
153
- begin
154
- while (line = stream.gets)
155
- buffer.(line)
156
-
157
- # control total time spent reading
158
- runtime = Time.now - Thread.current[:cmd_start]
159
- handle_timeout(runtime)
171
+ readers = [stream]
172
+
173
+ while readers.any?
174
+ ready = IO.select(readers, nil, readers, @timeout)
175
+ raise TimeoutExceeded if ready.nil?
176
+
177
+ ready[0].each do |reader|
178
+ begin
179
+ chunk = reader.readpartial(BUFSIZE)
180
+ handler.(chunk)
181
+
182
+ # control total time spent reading
183
+ runtime = Time.now - Thread.current[:cmd_start]
184
+ handle_timeout(runtime)
185
+ rescue Errno::EAGAIN, Errno::EINTR
186
+ rescue EOFError, Errno::EPIPE, Errno::EIO # thrown by PTY
187
+ readers.delete(reader)
188
+ reader.close
189
+ end
160
190
  end
161
- rescue Errno::EIO
162
- # GNU/Linux `gets` raises when PTY slave is closed
163
- nil
164
- rescue => err
165
- raise err
166
- ensure
167
- stream.close
168
191
  end
169
192
  end
170
193
  end
171
194
 
172
195
  # @api private
173
196
  def waitpid(pid)
174
- ::Process.waitpid(pid, Process::WUNTRACED)
175
- $?.exitstatus
197
+ _pid, status = ::Process.waitpid2(pid, ::Process::WUNTRACED)
198
+ status.exitstatus || status.termsig if _pid
176
199
  rescue Errno::ECHILD
177
200
  # In JRuby, waiting on a finished pid raises.
178
201
  end
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module TTY
@@ -1,4 +1,3 @@
1
- # encoding: utf-8
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module TTY
@@ -18,8 +17,8 @@ module TTY
18
17
  # @api public
19
18
  def initialize(options = {})
20
19
  @max_size = options.fetch(:max_size) { DEFAULT_SIZE }
21
- @prefix = ''
22
- @suffix = ''
20
+ @prefix = ""
21
+ @suffix = ""
23
22
  @skipped = 0
24
23
  end
25
24
 
@@ -93,7 +92,7 @@ module TTY
93
92
  # @api private
94
93
  def append(value, dst)
95
94
  remain = @max_size - dst.bytesize
96
- remaining = ''
95
+ remaining = ""
97
96
  if remain > 0
98
97
  value_bytes = value.to_s.bytesize
99
98
  offset = value_bytes < remain ? value_bytes : remain
@@ -1,7 +1,7 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  module TTY
4
4
  class Command
5
- VERSION = '0.7.0'
5
+ VERSION = "0.10.0"
6
6
  end # Command
7
7
  end # TTY
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tty-command
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Murach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-11-19 00:00:00.000000000 Z
11
+ date: 2020-10-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel
@@ -16,98 +16,57 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: 0.7.0
19
+ version: '0.8'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: 0.7.0
26
+ version: '0.8'
27
27
  - !ruby/object:Gem::Dependency
28
- name: bundler
28
+ name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 1.5.0
34
- - - "<"
35
- - !ruby/object:Gem::Version
36
- version: '2.0'
33
+ version: '0'
37
34
  type: :development
38
35
  prerelease: false
39
36
  version_requirements: !ruby/object:Gem::Requirement
40
37
  requirements:
41
38
  - - ">="
42
39
  - !ruby/object:Gem::Version
43
- version: 1.5.0
44
- - - "<"
45
- - !ruby/object:Gem::Version
46
- version: '2.0'
47
- - !ruby/object:Gem::Dependency
48
- name: rake
49
- requirement: !ruby/object:Gem::Requirement
50
- requirements:
51
- - - "~>"
52
- - !ruby/object:Gem::Version
53
- version: '10.0'
54
- type: :development
55
- prerelease: false
56
- version_requirements: !ruby/object:Gem::Requirement
57
- requirements:
58
- - - "~>"
59
- - !ruby/object:Gem::Version
60
- version: '10.0'
40
+ version: '0'
61
41
  - !ruby/object:Gem::Dependency
62
42
  name: rspec
63
43
  requirement: !ruby/object:Gem::Requirement
64
44
  requirements:
65
- - - "~>"
45
+ - - ">="
66
46
  - !ruby/object:Gem::Version
67
47
  version: '3.0'
68
48
  type: :development
69
49
  prerelease: false
70
50
  version_requirements: !ruby/object:Gem::Requirement
71
51
  requirements:
72
- - - "~>"
52
+ - - ">="
73
53
  - !ruby/object:Gem::Version
74
54
  version: '3.0'
75
55
  description: Execute shell commands with pretty output logging and capture their stdout,
76
56
  stderr and exit status. Redirect stdin, stdout and stderr of each command to a file
77
57
  or a string.
78
58
  email:
79
- - ''
59
+ - piotr@piotrmurach.com
80
60
  executables: []
81
61
  extensions: []
82
- extra_rdoc_files: []
62
+ extra_rdoc_files:
63
+ - README.md
64
+ - CHANGELOG.md
65
+ - LICENSE.txt
83
66
  files:
84
- - ".gitignore"
85
- - ".rspec"
86
- - ".travis.yml"
87
67
  - CHANGELOG.md
88
- - CODE_OF_CONDUCT.md
89
- - Gemfile
90
68
  - LICENSE.txt
91
69
  - README.md
92
- - Rakefile
93
- - appveyor.yml
94
- - benchmarks/memory.rb
95
- - bin/console
96
- - bin/setup
97
- - examples/bash.rb
98
- - examples/basic.rb
99
- - examples/cli
100
- - examples/env.rb
101
- - examples/logger.rb
102
- - examples/output.rb
103
- - examples/pty.rb
104
- - examples/redirect_stderr.rb
105
- - examples/redirect_stdin.rb
106
- - examples/redirect_stdout.rb
107
- - examples/stdin_input.rb
108
- - examples/threaded.rb
109
- - examples/timeout.rb
110
- - examples/wait.rb
111
70
  - lib/tty-command.rb
112
71
  - lib/tty/command.rb
113
72
  - lib/tty/command/child_process.rb
@@ -123,14 +82,16 @@ files:
123
82
  - lib/tty/command/result.rb
124
83
  - lib/tty/command/truncator.rb
125
84
  - lib/tty/command/version.rb
126
- - tasks/console.rake
127
- - tasks/coverage.rake
128
- - tasks/spec.rake
129
- - tty-command.gemspec
130
- homepage: https://piotrmurach.github.io/tty
85
+ homepage: https://ttytoolkit.org
131
86
  licenses:
132
87
  - MIT
133
- metadata: {}
88
+ metadata:
89
+ allowed_push_host: https://rubygems.org
90
+ bug_tracker_uri: https://github.com/piotrmurach/tty-command/issues
91
+ changelog_uri: https://github.com/piotrmurach/tty-command/blob/master/CHANGELOG.md
92
+ documentation_uri: https://www.rubydoc.info/gems/tty-command
93
+ homepage_uri: https://ttytoolkit.org
94
+ source_code_uri: https://github.com/piotrmurach/tty-command
134
95
  post_install_message:
135
96
  rdoc_options: []
136
97
  require_paths:
@@ -139,15 +100,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
139
100
  requirements:
140
101
  - - ">="
141
102
  - !ruby/object:Gem::Version
142
- version: '0'
103
+ version: 2.0.0
143
104
  required_rubygems_version: !ruby/object:Gem::Requirement
144
105
  requirements:
145
106
  - - ">="
146
107
  - !ruby/object:Gem::Version
147
108
  version: '0'
148
109
  requirements: []
149
- rubyforge_project:
150
- rubygems_version: 2.5.1
110
+ rubygems_version: 3.1.2
151
111
  signing_key:
152
112
  specification_version: 4
153
113
  summary: Execute shell commands with pretty output logging and capture their stdout,