tty-command 0.7.0 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fb721a5620ec91bddc77643358783a38fb76631f
4
- data.tar.gz: b0f3485e21f80e9172d1651eb661360fb3972d6f
3
+ metadata.gz: 0da26e779e4608d8ea38cf75801e0afa682dfd14
4
+ data.tar.gz: 1a8f798ca2de05727cd9217a20314a9730763cd9
5
5
  SHA512:
6
- metadata.gz: 3184987586dff4f957bbeee34cf0811f60c10db0f533fe48e957f171a3a1b22485f1e4970a7b4867466deb73ad95e9b3ae30c4194753724471756aece30446de
7
- data.tar.gz: 1500b854c1a69a788e49226d28403f92ee28258b608939596e1da92296ff815623ef70885e0c54751e3a8769539fc5f6c8dfd973f69c43c87f112cbf575707ce
6
+ metadata.gz: 6c966cec07e3ded53c89a294291e4ae9b74cdc7c0c81fb9a5843cd26bb0d7846fe14d57ae4b44756f8b5567fe40163bc27d7e33f2fedecb9826e97892002b9ba
7
+ data.tar.gz: d098129b2cda2da46cab16cc33f1086f7f1aa163b6c6d6a4e4d2a62103ba9e626704bfc2f275e7e6583cbeab3ee5a9352de48135d214b491caaeae8f550e000d
@@ -8,8 +8,9 @@ rvm:
8
8
  - 2.0.0
9
9
  - 2.1.10
10
10
  - 2.2.8
11
- - 2.3.5
12
- - 2.4.2
11
+ - 2.3.6
12
+ - 2.4.3
13
+ - 2.5.0
13
14
  - ruby-head
14
15
  - jruby-9.1.1.0
15
16
  - jruby-head
@@ -1,5 +1,19 @@
1
1
  # Change log
2
2
 
3
+ ## [v0.8.0] - 2018-04-22
4
+
5
+ ### Added
6
+ * Add :output_only_on_error option by Iulian Onofrei(@revolter)
7
+ * Add :verbose flag to toggle warnings
8
+
9
+ ### Changed
10
+ * Change ProcessRunner to use waitpid2 api for direct status
11
+ * Change ProcessRunner stdout & stderr reading to use IO.select and be non-blocking
12
+
13
+ ### Fixed
14
+ * Fix :timeout to raise when long running without input or output
15
+ * Fix ProcessRunner to ensure no zombie processes on timeouts
16
+
3
17
  ## [v0.7.0] - 2017-11-19
4
18
 
5
19
  ### Added
@@ -96,6 +110,7 @@
96
110
 
97
111
  * Initial implementation and release
98
112
 
113
+ [v0.8.0]: https://github.com/piotrmurach/tty-command/compare/v0.7.0...v0.8.0
99
114
  [v0.7.0]: https://github.com/piotrmurach/tty-command/compare/v0.6.0...v0.7.0
100
115
  [v0.6.0]: https://github.com/piotrmurach/tty-command/compare/v0.5.0...v0.6.0
101
116
  [v0.5.0]: https://github.com/piotrmurach/tty-command/compare/v0.4.0...v0.5.0
data/README.md CHANGED
@@ -52,6 +52,8 @@ Or install it yourself as:
52
52
  * [2.3. Logging](#23-logging)
53
53
  * [2.3.1. Color](#231-color)
54
54
  * [2.3.2. UUID](#232-uuid)
55
+ * [2.3.3. Only output on error](#233-only-output-on-error)
56
+ * [2.3.4. Verbose](#234-verbose)
55
57
  * [2.4. Dry run](#24-dry-run)
56
58
  * [2.5. Wait](#25-wait)
57
59
  * [2.6. Test](#26-test)
@@ -82,6 +84,8 @@ Or install it yourself as:
82
84
  Create a command instance and then run some commands:
83
85
 
84
86
  ```ruby
87
+ require 'tty-command'
88
+
85
89
  cmd = TTY::Command.new
86
90
  cmd.run('ls -la')
87
91
  cmd.run('echo Hello!')
@@ -188,18 +192,59 @@ cmd = TTY::Command.new(color: true)
188
192
 
189
193
  #### 2.3.1 Color
190
194
 
191
- When using printers you can switch off coloring by using `color` option set to `false`.
195
+ When using printers you can switch off coloring by using `:color` option set to `false`.
192
196
 
193
197
  #### 2.3.2 Uuid
194
198
 
195
- By default when logging is enabled each log entry is prefixed by specific command run uuid number. This number can be switched off using `uuid` option:
199
+ By default, when logging is enabled, each log entry is prefixed by specific command run uuid number. This number can be switched off using the `:uuid` option:
196
200
 
197
201
  ```ruby
198
- cmd = TTY::Command.new uuid: false
202
+ cmd = TTY::Command.new(uuid: false)
199
203
  cmd.run('rm -R all_my_files')
200
204
  # => rm -r all_my_files
201
205
  ```
202
206
 
207
+ #### 2.3.3 Only output on error
208
+
209
+ When using a command that can fail, setting `:only_output_on_error` option to `true` hides the output if the command succeeds:
210
+
211
+ ```ruby
212
+ cmd = TTY::Command.new
213
+ cmd.run('non_failing_command', only_output_on_error: true)
214
+ ```
215
+
216
+ This will only print the `Running` and `Finished` lines, while:
217
+
218
+ ```ruby
219
+ cmd.run('non_failing_command')
220
+ ```
221
+
222
+ will also print any output that the `non_failing_command` might generate.
223
+
224
+ Running either:
225
+
226
+ ```ruby
227
+ cmd.run('failing_command', only_output_on_error: true)
228
+ ```
229
+
230
+ either:
231
+
232
+ ```ruby
233
+ cmd.run('failing_command')
234
+ ```
235
+
236
+ will also print the output.
237
+
238
+ *Setting this option will cause the output to show at once, at the end of the command.*
239
+
240
+ #### 2.3.4 Verbose
241
+
242
+ By default commands will produce warnings when, for example `pty` option is not supported on a given platform. You can switch off such warnings with `:verbose` option set to `false`.
243
+
244
+ ```ruby
245
+ cmd.run("echo '\e[32mColors!\e[0m'", pty: true, verbose: false)
246
+ ```
247
+
203
248
  ### 2.4 Dry run
204
249
 
205
250
  Sometimes it can be useful to put your script into a "dry run" mode that prints commands without actually running them. To simulate execution of the command use the `:dry_run` option:
@@ -394,35 +439,37 @@ cmd.run("whilte test1; sleep1; done", timeout: 5, signal: :KILL)
394
439
 
395
440
  #### 3.2.6 PTY(pseudo terminal)
396
441
 
397
- The `:pty` configuration option causes the command to be executed in subprocess where each stream is a pseudo terminal. By default this options is set to `false`. However, some comamnds may require a terminal like device to work correctly. For example, a command may emit colored output only if it is running via terminal.
442
+ The `:pty` configuration option causes the command to be executed in subprocess where each stream is a `pseudo terminal`. By default this options is set to `false`.
443
+
444
+ If you require to interface with interactive subprocess then setting this option to `true` will enable a `pty` terminal device. For example, a command may emit colored output only if it is running via terminal device. You may also wish to run a program that waits for user input, and simulates typing in commands and reading responses.
445
+
446
+ This option will only work on systems that support BSD pty devices such as Linux or OS X, and it will gracefully fallback to non-pty device on all the other.
398
447
 
399
- In order to run command in pseudo terminal, either set the flag globally for all commands:
448
+ In order to run command in `pseudo terminal`, either set the flag globally for all commands:
400
449
 
401
450
  ```ruby
402
451
  cmd = TTY::Command.new(pty: true)
403
452
  ```
404
453
 
405
- or for each executed command individually:
454
+ or individually for each executed command:
406
455
 
407
456
  ```ruby
408
457
  cmd.run("echo 'hello'", pty: true)
409
458
  ```
410
459
 
411
- Please note though, that setting `:pty` to `true` may change how the command behaves. For instance, on unix like systems the line feed character `\n` in output will be prefixed with carriage return `\r`:
460
+ Please note that setting `:pty` to `true` may change how the command behaves. It's important to understand the difference between `interactive` and `non-interactive` modes. For example, executing `git log` to view the commit history in default `non-interactive` mode:
412
461
 
413
462
  ```ruby
414
- out, _ = cmd.run("echo 'hello'")
415
- out # => "hello\n"
463
+ cmd.run("git log") # => finishes and produces full output
416
464
  ```
417
465
 
418
- and with `:pty` option:
466
+ However, in `interactive` mode with `pty` flag on:
419
467
 
420
468
  ```ruby
421
- out, _ = cmd.run("echo 'hello'", pty: true)
422
- out # => "hello\r\n"
469
+ cmd.run("git log", pty: true) # => uses pager and waits for user input (never returns)
423
470
  ```
424
471
 
425
- In addition, any input to command may be echoed to the standard output.
472
+ In addition, when pty device is used, any input to command may be echoed to the standard output, as well as some redirets may not work.
426
473
 
427
474
  #### 3.2.7 Current directory
428
475
 
@@ -588,4 +635,4 @@ The gem is available as open source under the terms of the [MIT License](http://
588
635
 
589
636
  ## Copyright
590
637
 
591
- Copyright (c) 2016-2017 Piotr Murach. See LICENSE for further details.
638
+ Copyright (c) 2016-2018 Piotr Murach. See LICENSE for further details.
@@ -19,6 +19,8 @@ environment:
19
19
  - ruby_version: "23-x64"
20
20
  - ruby_version: "24"
21
21
  - ruby_version: "24-x64"
22
+ - ruby_version: "25"
23
+ - ruby_version: "25-x64"
22
24
  matrix:
23
25
  allow_failures:
24
- - ruby_version: "193"
26
+ - ruby_version: "25"
@@ -4,6 +4,6 @@ require 'tty-command'
4
4
 
5
5
  cmd = TTY::Command.new
6
6
 
7
- out, err = cmd.run(:echo, "$FOO", env: { foo: 'hello'})
7
+ out, err = cmd.run("env | grep FOO", env: { 'FOO' =>'hello'})
8
8
 
9
9
  puts "Result: #{out}"
@@ -1,9 +1,8 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  require 'tty-command'
4
- require 'pathname'
5
4
 
6
- cli = Pathname.new('examples/cli.rb')
5
+ cli = File.expand_path('cli', __dir__)
7
6
  cmd = TTY::Command.new
8
7
 
9
8
  stdin = StringIO.new
@@ -59,6 +59,7 @@ module TTY
59
59
  @dry_run = options.fetch(:dry_run) { false }
60
60
  @printer = use_printer(@printer_name, color: @color, uuid: @uuid)
61
61
  @cmd_options = {}
62
+ @cmd_options[:verbose] = options.fetch(:verbose, true)
62
63
  @cmd_options[:pty] = true if options[:pty]
63
64
  @cmd_options[:binmode] = true if options[:binmode]
64
65
  @cmd_options[:timeout] = options[:timeout] if options[:timeout]
@@ -203,7 +204,7 @@ module TTY
203
204
  #
204
205
  # @api private
205
206
  def find_printer_class(name)
206
- const_name = name.to_s.capitalize.to_sym
207
+ const_name = name.to_s.split('_').map(&:capitalize).join.to_sym
207
208
  if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name)
208
209
  raise ArgumentError, %(Unknown printer type "#{name}")
209
210
  end
@@ -25,8 +25,9 @@ module TTY
25
25
  process_opts = normalize_redirect_options(cmd.options)
26
26
  binmode = cmd.options[:binmode] || false
27
27
  pty = cmd.options[:pty] || false
28
+ verbose = cmd.options[:verbose]
28
29
 
29
- pty = try_loading_pty if pty
30
+ pty = try_loading_pty(verbose) if pty
30
31
  require('pty') if pty # load within this scope
31
32
 
32
33
  # Create pipes
@@ -65,8 +66,8 @@ module TTY
65
66
 
66
67
  pid = Process.spawn(cmd.to_command, opts)
67
68
 
68
- # close in parent process
69
- [in_rd, out_wr, err_wr].each { |fd| fd.close if fd }
69
+ # close streams in parent process talking to the child
70
+ close_fds(in_rd, out_wr, err_wr)
70
71
 
71
72
  tuple = [pid, in_wr, out_rd, err_rd]
72
73
 
@@ -75,7 +76,7 @@ module TTY
75
76
  return yield(*tuple)
76
77
  ensure
77
78
  # ensure parent pipes are closed
78
- [in_wr, out_rd, err_rd].each { |fd| fd.close if fd && !fd.closed? }
79
+ close_fds(in_wr, out_rd, err_rd)
79
80
  end
80
81
  else
81
82
  tuple
@@ -83,16 +84,23 @@ module TTY
83
84
  end
84
85
  module_function :spawn
85
86
 
87
+ # Close all streams
88
+ # @api private
89
+ def close_fds(*fds)
90
+ fds.each { |fd| fd && !fd.closed? && fd.close }
91
+ end
92
+ module_function :close_fds
93
+
86
94
  # Try loading pty module
87
95
  #
88
96
  # @return [Boolean]
89
97
  #
90
98
  # @api private
91
- def try_loading_pty
99
+ def try_loading_pty(verbose = false)
92
100
  require 'pty'
93
101
  true
94
102
  rescue LoadError
95
- warn("Requested PTY device but the system doesn't support it.")
103
+ warn("Requested PTY device but the system doesn't support it.") if verbose
96
104
  false
97
105
  end
98
106
  module_function :try_loading_pty
@@ -22,6 +22,9 @@ module TTY
22
22
  # @api public
23
23
  attr_reader :uuid
24
24
 
25
+ # Flag that controls whether to print the output only on error or not
26
+ attr_reader :only_output_on_error
27
+
25
28
  # Initialize a new Cmd object
26
29
  #
27
30
  # @api private
@@ -53,6 +56,7 @@ module TTY
53
56
  @options = opts
54
57
 
55
58
  @uuid = SecureRandom.uuid.split('-')[0]
59
+ @only_output_on_error = opts.fetch(:only_output_on_error) { false }
56
60
  freeze
57
61
  end
58
62
 
@@ -20,7 +20,7 @@ module TTY
20
20
  cmd.to_command
21
21
  message = "#{@printer.decorate('(dry run)', :blue)} " +
22
22
  @printer.decorate(cmd.to_command, :yellow, :bold)
23
- @printer.write(message, cmd.uuid)
23
+ @printer.write(cmd, message, cmd.uuid)
24
24
  Result.new(0, '', '')
25
25
  end
26
26
  end # DryRunner
@@ -11,6 +11,7 @@ module TTY
11
11
  def_delegators :@color, :decorate
12
12
 
13
13
  attr_reader :output, :options
14
+ attr_accessor :out_data, :err_data
14
15
 
15
16
  # Initialize a Printer object
16
17
  #
@@ -21,8 +22,11 @@ module TTY
21
22
  def initialize(output, options = {})
22
23
  @output = output
23
24
  @options = options
24
- @enabled = options.fetch(:color) { true }
25
+ @enabled = options.fetch(:color) { true }
25
26
  @color = ::Pastel.new(output: output, enabled: @enabled)
27
+
28
+ @out_data = ''
29
+ @err_data = ''
26
30
  end
27
31
 
28
32
  def print_command_start(cmd, *args)
@@ -41,7 +45,7 @@ module TTY
41
45
  write(args.join(' '))
42
46
  end
43
47
 
44
- def write(message)
48
+ def write(cmd, message)
45
49
  raise NotImplemented, "Abstract printer cannot be used"
46
50
  end
47
51
  end # Abstract
@@ -14,38 +14,44 @@ module TTY
14
14
  def print_command_start(cmd, *args)
15
15
  message = ["Running #{decorate(cmd.to_command, :yellow, :bold)}"]
16
16
  message << args.map(&:chomp).join(' ') unless args.empty?
17
- write(message.join, cmd.uuid)
17
+ write(cmd, message.join, cmd.uuid)
18
18
  end
19
19
 
20
20
  def print_command_out_data(cmd, *args)
21
21
  message = args.map(&:chomp).join(' ')
22
- write("\t#{message}", cmd.uuid)
22
+ write(cmd, "\t#{message}", cmd.uuid, out_data)
23
23
  end
24
24
 
25
25
  def print_command_err_data(cmd, *args)
26
26
  message = args.map(&:chomp).join(' ')
27
- write("\t" + decorate(message, :red), cmd.uuid)
27
+ write(cmd, "\t" + decorate(message, :red), cmd.uuid, err_data)
28
28
  end
29
29
 
30
30
  def print_command_exit(cmd, status, runtime, *args)
31
+ unless !cmd.only_output_on_error || status.zero?
32
+ output << out_data
33
+ output << err_data
34
+ end
35
+
31
36
  runtime = TIME_FORMAT % [runtime, pluralize(runtime, 'second')]
32
37
  message = ["Finished in #{runtime}"]
33
38
  message << " with exit status #{status}" if status
34
39
  message << " (#{success_or_failure(status)})"
35
- write(message.join, cmd.uuid)
40
+ write(cmd, message.join, cmd.uuid)
36
41
  end
37
42
 
38
43
  # Write message out to output
39
44
  #
40
45
  # @api private
41
- def write(message, uuid = nil)
46
+ def write(cmd, message, uuid = nil, data = nil)
42
47
  uuid_needed = options.fetch(:uuid) { true }
43
48
  out = []
44
49
  if uuid_needed
45
50
  out << "[#{decorate(uuid, :green)}] " unless uuid.nil?
46
51
  end
47
52
  out << "#{message}\n"
48
- output << out.join
53
+ target = (cmd.only_output_on_error && !data.nil?) ? data : output
54
+ target << out.join
49
55
  end
50
56
 
51
57
  private
@@ -12,12 +12,26 @@ module TTY
12
12
  # quiet
13
13
  end
14
14
 
15
- def print_command_exit(cmd, *args)
15
+ def print_command_out_data(cmd, *args)
16
+ write(cmd, args.join(' '), out_data)
17
+ end
18
+
19
+ def print_command_err_data(cmd, *args)
20
+ write(cmd, args.join(' '), err_data)
21
+ end
22
+
23
+ def print_command_exit(cmd, status, *args)
24
+ unless !cmd.only_output_on_error || status.zero?
25
+ output << out_data
26
+ output << err_data
27
+ end
28
+
16
29
  # quiet
17
30
  end
18
31
 
19
- def write(message)
20
- output << message
32
+ def write(cmd, message, data = nil)
33
+ target = (cmd.only_output_on_error && !data.nil?) ? data : output
34
+ target << message
21
35
  end
22
36
  end # Progress
23
37
  end # Printers
@@ -23,7 +23,8 @@ module TTY
23
23
  @cmd = cmd
24
24
  @timeout = cmd.options[:timeout]
25
25
  @input = cmd.options[:input]
26
- @signal = cmd.options[:signal] || :TERM
26
+ @signal = cmd.options[:signal] || "SIGKILL"
27
+ @binmode = cmd.options[:binmode]
27
28
  @printer = printer
28
29
  @block = block
29
30
  end
@@ -40,21 +41,19 @@ module TTY
40
41
  def run!
41
42
  @printer.print_command_start(cmd)
42
43
  start = Time.now
43
- runtime = 0.0
44
44
 
45
45
  pid, stdin, stdout, stderr = ChildProcess.spawn(cmd)
46
46
 
47
47
  # no input to write, close child's stdin pipe
48
48
  stdin.close if (@input.nil? || @input.empty?) && !stdin.nil?
49
49
 
50
- readers = [stdout, stderr]
51
50
  writers = [@input && stdin].compact
52
51
 
53
52
  while writers.any?
54
- ready_readers, ready_writers = IO.select(readers, writers, [], @timeout)
55
- raise TimeoutExceeded if ready_readers.nil? || ready_writers.nil?
53
+ ready = IO.select(nil, writers, writers, @timeout)
54
+ raise TimeoutExceeded if ready.nil?
56
55
 
57
- write_stream(ready_writers, writers)
56
+ write_stream(ready[1], writers)
58
57
  end
59
58
 
60
59
  stdout_data, stderr_data = read_streams(stdout, stderr)
@@ -67,6 +66,10 @@ module TTY
67
66
  Result.new(status, stdout_data, stderr_data, runtime)
68
67
  ensure
69
68
  [stdin, stdout, stderr].each { |fd| fd.close if fd && !fd.closed? }
69
+ if pid # Ensure no zombie processes
70
+ ::Process.detach(pid)
71
+ terminate(pid)
72
+ end
70
73
  end
71
74
 
72
75
  # Stop a process marked by pid
@@ -80,6 +83,9 @@ module TTY
80
83
 
81
84
  private
82
85
 
86
+ # The buffer size for reading stdout and stderr
87
+ BUFSIZE = 3 * 1024
88
+
83
89
  # @api private
84
90
  def handle_timeout(runtime)
85
91
  return unless @timeout
@@ -126,13 +132,13 @@ module TTY
126
132
  stdout_data = []
127
133
  stderr_data = Truncator.new
128
134
 
129
- out_buffer = -> (line) {
135
+ out_buffer = ->(line) {
130
136
  stdout_data << line
131
137
  @printer.print_command_out_data(cmd, line)
132
138
  @block.(line, nil) if @block
133
139
  }
134
140
 
135
- err_buffer = -> (line) {
141
+ err_buffer = ->(line) {
136
142
  stderr_data << line
137
143
  @printer.print_command_err_data(cmd, line)
138
144
  @block.(nil, line) if @block
@@ -144,35 +150,45 @@ module TTY
144
150
  stdout_thread.join
145
151
  stderr_thread.join
146
152
 
147
- [stdout_data.join, stderr_data.read]
153
+ encoding = @binmode ? Encoding::BINARY : Encoding::UTF_8
154
+
155
+ [
156
+ stdout_data.join.force_encoding(encoding),
157
+ stderr_data.read.dup.force_encoding(encoding)
158
+ ]
148
159
  end
149
160
 
150
161
  def read_stream(stream, buffer)
151
162
  Thread.new do
152
163
  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)
164
+ readers = [stream]
165
+
166
+ while readers.any?
167
+ ready = IO.select(readers, nil, readers, @timeout)
168
+ raise TimeoutExceeded if ready.nil?
169
+
170
+ ready[0].each do |reader|
171
+ begin
172
+ line = reader.readpartial(BUFSIZE)
173
+ buffer.(line)
174
+
175
+ # control total time spent reading
176
+ runtime = Time.now - Thread.current[:cmd_start]
177
+ handle_timeout(runtime)
178
+ rescue Errno::EAGAIN, Errno::EINTR
179
+ rescue EOFError, Errno::EPIPE, Errno::EIO # thrown by PTY
180
+ readers.delete(reader)
181
+ reader.close
182
+ end
160
183
  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
184
  end
169
185
  end
170
186
  end
171
187
 
172
188
  # @api private
173
189
  def waitpid(pid)
174
- ::Process.waitpid(pid, Process::WUNTRACED)
175
- $?.exitstatus
190
+ _pid, status = ::Process.waitpid2(pid, ::Process::WUNTRACED)
191
+ status.exitstatus || status.termsig if _pid
176
192
  rescue Errno::ECHILD
177
193
  # In JRuby, waiting on a finished pid raises.
178
194
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module TTY
4
4
  class Command
5
- VERSION = '0.7.0'
5
+ VERSION = '0.8.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.8.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: 2018-04-22 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: pastel