tty-command 0.7.0 → 0.8.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 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