outstand-tty-command 0.10.0.pre
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 +7 -0
- data/CHANGELOG.md +156 -0
- data/LICENSE.txt +21 -0
- data/README.md +661 -0
- data/lib/tty-command.rb +1 -0
- data/lib/tty/command.rb +223 -0
- data/lib/tty/command/child_process.rb +221 -0
- data/lib/tty/command/cmd.rb +148 -0
- data/lib/tty/command/dry_runner.rb +27 -0
- data/lib/tty/command/exit_error.rb +31 -0
- data/lib/tty/command/printers/abstract.rb +54 -0
- data/lib/tty/command/printers/null.rb +16 -0
- data/lib/tty/command/printers/pretty.rb +83 -0
- data/lib/tty/command/printers/progress.rb +32 -0
- data/lib/tty/command/printers/quiet.rb +39 -0
- data/lib/tty/command/process_runner.rb +196 -0
- data/lib/tty/command/result.rb +89 -0
- data/lib/tty/command/truncator.rb +106 -0
- data/lib/tty/command/version.rb +7 -0
- metadata +129 -0
data/lib/tty-command.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'tty/command'
|
data/lib/tty/command.rb
ADDED
@@ -0,0 +1,223 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rbconfig'
|
4
|
+
|
5
|
+
require_relative 'command/cmd'
|
6
|
+
require_relative 'command/exit_error'
|
7
|
+
require_relative 'command/dry_runner'
|
8
|
+
require_relative 'command/process_runner'
|
9
|
+
require_relative 'command/printers/null'
|
10
|
+
require_relative 'command/printers/pretty'
|
11
|
+
require_relative 'command/printers/progress'
|
12
|
+
require_relative 'command/printers/quiet'
|
13
|
+
require_relative 'command/version'
|
14
|
+
|
15
|
+
module TTY
|
16
|
+
class Command
|
17
|
+
ExecuteError = Class.new(StandardError)
|
18
|
+
|
19
|
+
TimeoutExceeded = Class.new(StandardError)
|
20
|
+
|
21
|
+
# Path to the current Ruby
|
22
|
+
RUBY = ENV['RUBY'] || ::File.join(
|
23
|
+
RbConfig::CONFIG['bindir'],
|
24
|
+
RbConfig::CONFIG['ruby_install_name'] + RbConfig::CONFIG['EXEEXT']
|
25
|
+
)
|
26
|
+
|
27
|
+
WIN_PLATFORMS = /cygwin|mswin|mingw|bccwin|wince|emx/.freeze
|
28
|
+
|
29
|
+
def self.record_separator
|
30
|
+
@record_separator ||= $/
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.record_separator=(sep)
|
34
|
+
@record_separator = sep
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.windows?
|
38
|
+
!!(RbConfig::CONFIG['host_os'] =~ WIN_PLATFORMS)
|
39
|
+
end
|
40
|
+
|
41
|
+
attr_reader :printer
|
42
|
+
|
43
|
+
# Initialize a Command object
|
44
|
+
#
|
45
|
+
# @param [Hash] options
|
46
|
+
# @option options [IO] :output
|
47
|
+
# the stream to which printer prints, defaults to stdout
|
48
|
+
# @option options [Symbol] :printer
|
49
|
+
# the printer to use for output logging, defaults to :pretty
|
50
|
+
# @option options [Symbol] :dry_run
|
51
|
+
# the mode for executing command
|
52
|
+
#
|
53
|
+
# @api public
|
54
|
+
def initialize(**options)
|
55
|
+
@output = options.fetch(:output) { $stdout }
|
56
|
+
@color = options.fetch(:color) { true }
|
57
|
+
@uuid = options.fetch(:uuid) { true }
|
58
|
+
@printer_name = options.fetch(:printer) { :pretty }
|
59
|
+
@dry_run = options.fetch(:dry_run) { false }
|
60
|
+
@printer = use_printer(@printer_name, color: @color, uuid: @uuid)
|
61
|
+
@cmd_options = {}
|
62
|
+
@cmd_options[:verbose] = options.fetch(:verbose, true)
|
63
|
+
@cmd_options[:pty] = true if options[:pty]
|
64
|
+
@cmd_options[:binmode] = true if options[:binmode]
|
65
|
+
@cmd_options[:timeout] = options[:timeout] if options[:timeout]
|
66
|
+
end
|
67
|
+
|
68
|
+
# Start external executable in a child process
|
69
|
+
#
|
70
|
+
# @example
|
71
|
+
# cmd.run(command, [argv1, ..., argvN], [options])
|
72
|
+
#
|
73
|
+
# @example
|
74
|
+
# cmd.run(command, ...) do |result|
|
75
|
+
# ...
|
76
|
+
# end
|
77
|
+
#
|
78
|
+
# @param [String] command
|
79
|
+
# the command to run
|
80
|
+
#
|
81
|
+
# @param [Array[String]] argv
|
82
|
+
# an array of string arguments
|
83
|
+
#
|
84
|
+
# @param [Hash] options
|
85
|
+
# hash of operations to perform
|
86
|
+
# @option options [String] :chdir
|
87
|
+
# The current directory.
|
88
|
+
# @option options [Integer] :timeout
|
89
|
+
# Maximum number of seconds to allow the process
|
90
|
+
# to run before aborting with a TimeoutExceeded
|
91
|
+
# exception.
|
92
|
+
# @option options [Symbol] :signal
|
93
|
+
# Signal used on timeout, SIGKILL by default
|
94
|
+
#
|
95
|
+
# @yield [out, err]
|
96
|
+
# Yields stdout and stderr output whenever available
|
97
|
+
#
|
98
|
+
# @raise [ExitError]
|
99
|
+
# raised when command exits with non-zero code
|
100
|
+
#
|
101
|
+
# @api public
|
102
|
+
def run(*args, &block)
|
103
|
+
cmd = command(*args)
|
104
|
+
result = execute_command(cmd, &block)
|
105
|
+
if result && result.failure?
|
106
|
+
raise ExitError.new(cmd.to_command, result)
|
107
|
+
end
|
108
|
+
result
|
109
|
+
end
|
110
|
+
|
111
|
+
# Start external executable without raising ExitError
|
112
|
+
#
|
113
|
+
# @example
|
114
|
+
# cmd.run!(command, [argv1, ..., argvN], [options])
|
115
|
+
#
|
116
|
+
# @api public
|
117
|
+
def run!(*args, &block)
|
118
|
+
cmd = command(*args)
|
119
|
+
execute_command(cmd, &block)
|
120
|
+
end
|
121
|
+
|
122
|
+
# Wait on long running script until output matches a specific pattern
|
123
|
+
#
|
124
|
+
# @example
|
125
|
+
# cmd.wait 'tail -f /var/log/php.log', /something happened/
|
126
|
+
#
|
127
|
+
# @api public
|
128
|
+
def wait(*args)
|
129
|
+
pattern = args.pop
|
130
|
+
unless pattern
|
131
|
+
raise ArgumentError, 'Please provide condition to wait for'
|
132
|
+
end
|
133
|
+
|
134
|
+
run(*args) do |out, _|
|
135
|
+
raise if out =~ /#{pattern}/
|
136
|
+
end
|
137
|
+
rescue ExitError
|
138
|
+
# noop
|
139
|
+
end
|
140
|
+
|
141
|
+
# Execute shell test command
|
142
|
+
#
|
143
|
+
# @api public
|
144
|
+
def test(*args)
|
145
|
+
run!(:test, *args).success?
|
146
|
+
end
|
147
|
+
|
148
|
+
# Run Ruby interperter with the given arguments
|
149
|
+
#
|
150
|
+
# @example
|
151
|
+
# ruby %q{-e "puts 'Hello world'"}
|
152
|
+
#
|
153
|
+
# @api public
|
154
|
+
def ruby(*args, &block)
|
155
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
156
|
+
if args.length > 1
|
157
|
+
run(*([RUBY] + args + [options]), &block)
|
158
|
+
else
|
159
|
+
run("#{RUBY} #{args.first}", options, &block)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
# Check if in dry mode
|
164
|
+
#
|
165
|
+
# @return [Boolean]
|
166
|
+
#
|
167
|
+
# @public
|
168
|
+
def dry_run?
|
169
|
+
@dry_run
|
170
|
+
end
|
171
|
+
|
172
|
+
private
|
173
|
+
|
174
|
+
# @api private
|
175
|
+
def command(*args)
|
176
|
+
cmd = Cmd.new(*args)
|
177
|
+
cmd.update(**@cmd_options)
|
178
|
+
cmd
|
179
|
+
end
|
180
|
+
|
181
|
+
# @api private
|
182
|
+
def execute_command(cmd, &block)
|
183
|
+
dry_run = @dry_run || cmd.options[:dry_run] || false
|
184
|
+
@runner = select_runner(dry_run).new(cmd, @printer, &block)
|
185
|
+
@runner.run!
|
186
|
+
end
|
187
|
+
|
188
|
+
# @api private
|
189
|
+
def use_printer(class_or_name, options)
|
190
|
+
if class_or_name.is_a?(TTY::Command::Printers::Abstract)
|
191
|
+
return class_or_name
|
192
|
+
end
|
193
|
+
|
194
|
+
if class_or_name.is_a?(Class)
|
195
|
+
class_or_name
|
196
|
+
else
|
197
|
+
find_printer_class(class_or_name)
|
198
|
+
end.new(@output, options)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Find printer class or fail
|
202
|
+
#
|
203
|
+
# @raise [ArgumentError]
|
204
|
+
#
|
205
|
+
# @api private
|
206
|
+
def find_printer_class(name)
|
207
|
+
const_name = name.to_s.split('_').map(&:capitalize).join.to_sym
|
208
|
+
if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name)
|
209
|
+
raise ArgumentError, %(Unknown printer type "#{name}")
|
210
|
+
end
|
211
|
+
TTY::Command::Printers.const_get(const_name)
|
212
|
+
end
|
213
|
+
|
214
|
+
# @api private
|
215
|
+
def select_runner(dry_run)
|
216
|
+
if dry_run
|
217
|
+
DryRunner
|
218
|
+
else
|
219
|
+
ProcessRunner
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end # Command
|
223
|
+
end # TTY
|
@@ -0,0 +1,221 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
require 'securerandom'
|
5
|
+
require 'io/console'
|
6
|
+
|
7
|
+
module TTY
|
8
|
+
class Command
|
9
|
+
module ChildProcess
|
10
|
+
# Execute command in a child process with all IO streams piped
|
11
|
+
# in and out. The interface is similar to Process.spawn
|
12
|
+
#
|
13
|
+
# The caller should ensure that all IO objects are closed
|
14
|
+
# when the child process is finished. However, when block
|
15
|
+
# is provided this will be taken care of automatically.
|
16
|
+
#
|
17
|
+
# @param [Cmd] cmd
|
18
|
+
# the command to spawn
|
19
|
+
#
|
20
|
+
# @return [pid, stdin, stdout, stderr]
|
21
|
+
#
|
22
|
+
# @api public
|
23
|
+
def spawn(cmd)
|
24
|
+
process_opts = normalize_redirect_options(cmd.options)
|
25
|
+
binmode = cmd.options[:binmode] || false
|
26
|
+
pty = cmd.options[:pty] || false
|
27
|
+
verbose = cmd.options[:verbose]
|
28
|
+
|
29
|
+
pty = try_loading_pty(verbose) if pty
|
30
|
+
require('pty') if pty # load within this scope
|
31
|
+
|
32
|
+
# Create pipes
|
33
|
+
in_rd, in_wr = pty ? PTY.open : IO.pipe('utf-8') # reading
|
34
|
+
out_rd, out_wr = pty ? PTY.open : IO.pipe('utf-8') # writing
|
35
|
+
err_rd, err_wr = pty ? PTY.open : IO.pipe('utf-8') # error
|
36
|
+
in_wr.sync = true
|
37
|
+
|
38
|
+
if binmode
|
39
|
+
in_wr.binmode
|
40
|
+
out_rd.binmode
|
41
|
+
err_rd.binmode
|
42
|
+
end
|
43
|
+
|
44
|
+
if pty
|
45
|
+
in_wr.raw!
|
46
|
+
out_wr.raw!
|
47
|
+
err_wr.raw!
|
48
|
+
end
|
49
|
+
|
50
|
+
# redirect fds
|
51
|
+
opts = {
|
52
|
+
in: in_rd,
|
53
|
+
out: out_wr,
|
54
|
+
err: err_wr
|
55
|
+
}
|
56
|
+
unless TTY::Command.windows?
|
57
|
+
close_child_fds = {
|
58
|
+
in_wr => :close,
|
59
|
+
out_rd => :close,
|
60
|
+
err_rd => :close
|
61
|
+
}
|
62
|
+
opts.merge!(close_child_fds)
|
63
|
+
end
|
64
|
+
opts.merge!(process_opts)
|
65
|
+
|
66
|
+
pid = Process.spawn(cmd.to_command, opts)
|
67
|
+
|
68
|
+
# close streams in parent process talking to the child
|
69
|
+
close_fds(in_rd, out_wr, err_wr)
|
70
|
+
|
71
|
+
tuple = [pid, in_wr, out_rd, err_rd]
|
72
|
+
|
73
|
+
if block_given?
|
74
|
+
begin
|
75
|
+
return yield(*tuple)
|
76
|
+
ensure
|
77
|
+
# ensure parent pipes are closed
|
78
|
+
close_fds(in_wr, out_rd, err_rd)
|
79
|
+
end
|
80
|
+
else
|
81
|
+
tuple
|
82
|
+
end
|
83
|
+
end
|
84
|
+
module_function :spawn
|
85
|
+
|
86
|
+
# Close all streams
|
87
|
+
# @api private
|
88
|
+
def close_fds(*fds)
|
89
|
+
fds.each { |fd| fd && !fd.closed? && fd.close }
|
90
|
+
end
|
91
|
+
module_function :close_fds
|
92
|
+
|
93
|
+
# Try loading pty module
|
94
|
+
#
|
95
|
+
# @return [Boolean]
|
96
|
+
#
|
97
|
+
# @api private
|
98
|
+
def try_loading_pty(verbose = false)
|
99
|
+
require 'pty'
|
100
|
+
true
|
101
|
+
rescue LoadError
|
102
|
+
warn("Requested PTY device but the system doesn't support it.") if verbose
|
103
|
+
false
|
104
|
+
end
|
105
|
+
module_function :try_loading_pty
|
106
|
+
|
107
|
+
# Normalize spawn fd into :in, :out, :err keys.
|
108
|
+
#
|
109
|
+
# @return [Hash]
|
110
|
+
#
|
111
|
+
# @api private
|
112
|
+
def normalize_redirect_options(options)
|
113
|
+
options.reduce({}) do |opts, (key, value)|
|
114
|
+
if fd?(key)
|
115
|
+
spawn_key, spawn_value = convert(key, value)
|
116
|
+
opts[spawn_key] = spawn_value
|
117
|
+
elsif key.is_a?(Array) && key.all?(&method(:fd?))
|
118
|
+
key.each do |k|
|
119
|
+
spawn_key, spawn_value = convert(k, value)
|
120
|
+
opts[spawn_key] = spawn_value
|
121
|
+
end
|
122
|
+
end
|
123
|
+
opts
|
124
|
+
end
|
125
|
+
end
|
126
|
+
module_function :normalize_redirect_options
|
127
|
+
|
128
|
+
# Convert option pari to recognized spawn option pair
|
129
|
+
#
|
130
|
+
# @api private
|
131
|
+
def convert(spawn_key, spawn_value)
|
132
|
+
key = fd_to_process_key(spawn_key)
|
133
|
+
value = spawn_value
|
134
|
+
|
135
|
+
if key.to_s == 'in'
|
136
|
+
value = convert_to_fd(spawn_value)
|
137
|
+
end
|
138
|
+
|
139
|
+
if fd?(spawn_value)
|
140
|
+
value = fd_to_process_key(spawn_value)
|
141
|
+
value = [:child, value] # redirect in child process
|
142
|
+
end
|
143
|
+
[key, value]
|
144
|
+
end
|
145
|
+
module_function :convert
|
146
|
+
|
147
|
+
# Determine if object is a fd
|
148
|
+
#
|
149
|
+
# @return [Boolean]
|
150
|
+
#
|
151
|
+
# @api private
|
152
|
+
def fd?(object)
|
153
|
+
case object
|
154
|
+
when :stdin, :stdout, :stderr, :in, :out, :err,
|
155
|
+
STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, ::IO
|
156
|
+
true
|
157
|
+
when ::Integer
|
158
|
+
object >= 0
|
159
|
+
else
|
160
|
+
respond_to?(:to_i) && !object.to_io.nil?
|
161
|
+
end
|
162
|
+
end
|
163
|
+
module_function :fd?
|
164
|
+
|
165
|
+
# Convert fd to name :in, :out, :err
|
166
|
+
#
|
167
|
+
# @api private
|
168
|
+
def fd_to_process_key(object)
|
169
|
+
case object
|
170
|
+
when STDIN, $stdin, :in, :stdin, 0
|
171
|
+
:in
|
172
|
+
when STDOUT, $stdout, :out, :stdout, 1
|
173
|
+
:out
|
174
|
+
when STDERR, $stderr, :err, :stderr, 2
|
175
|
+
:err
|
176
|
+
when Integer
|
177
|
+
object >= 0 ? IO.for_fd(object) : nil
|
178
|
+
when IO
|
179
|
+
object
|
180
|
+
when respond_to?(:to_io)
|
181
|
+
object.to_io
|
182
|
+
else
|
183
|
+
raise ExecuteError, "Wrong execute redirect: #{object.inspect}"
|
184
|
+
end
|
185
|
+
end
|
186
|
+
module_function :fd_to_process_key
|
187
|
+
|
188
|
+
# Convert file name to file handle
|
189
|
+
#
|
190
|
+
# @api private
|
191
|
+
def convert_to_fd(object)
|
192
|
+
return object if fd?(object)
|
193
|
+
|
194
|
+
if object.is_a?(::String) && ::File.exist?(object)
|
195
|
+
return object
|
196
|
+
end
|
197
|
+
|
198
|
+
tmp = ::Tempfile.new(::SecureRandom.uuid.split('-')[0])
|
199
|
+
content = try_reading(object)
|
200
|
+
tmp.write(content)
|
201
|
+
tmp.rewind
|
202
|
+
tmp
|
203
|
+
end
|
204
|
+
module_function :convert_to_fd
|
205
|
+
|
206
|
+
# Attempts to read object content
|
207
|
+
#
|
208
|
+
# @api private
|
209
|
+
def try_reading(object)
|
210
|
+
if object.respond_to?(:read)
|
211
|
+
object.read
|
212
|
+
elsif object.respond_to?(:to_s)
|
213
|
+
object.to_s
|
214
|
+
else
|
215
|
+
object
|
216
|
+
end
|
217
|
+
end
|
218
|
+
module_function :try_reading
|
219
|
+
end # ChildProcess
|
220
|
+
end # Command
|
221
|
+
end # TTY
|
@@ -0,0 +1,148 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'securerandom'
|
4
|
+
require 'shellwords'
|
5
|
+
|
6
|
+
module TTY
|
7
|
+
class Command
|
8
|
+
class Cmd
|
9
|
+
# A string command name, or shell program
|
10
|
+
# @api public
|
11
|
+
attr_reader :command
|
12
|
+
|
13
|
+
# A string arguments
|
14
|
+
# @api public
|
15
|
+
attr_reader :argv
|
16
|
+
|
17
|
+
# Hash of operations to peform
|
18
|
+
# @api public
|
19
|
+
attr_reader :options
|
20
|
+
|
21
|
+
# Unique identifier
|
22
|
+
# @api public
|
23
|
+
attr_reader :uuid
|
24
|
+
|
25
|
+
# Flag that controls whether to print the output only on error or not
|
26
|
+
attr_reader :only_output_on_error
|
27
|
+
|
28
|
+
# Initialize a new Cmd object
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
def initialize(env_or_cmd, *args)
|
32
|
+
opts = args.last.respond_to?(:to_hash) ? args.pop : {}
|
33
|
+
if env_or_cmd.respond_to?(:to_hash)
|
34
|
+
@env = env_or_cmd
|
35
|
+
unless command = args.shift
|
36
|
+
raise ArgumentError, 'Cmd requires command argument'
|
37
|
+
end
|
38
|
+
else
|
39
|
+
command = env_or_cmd
|
40
|
+
end
|
41
|
+
|
42
|
+
if args.empty? && cmd = command.to_s
|
43
|
+
raise ArgumentError, 'No command provided' if cmd.empty?
|
44
|
+
@command = sanitize(cmd)
|
45
|
+
@argv = []
|
46
|
+
else
|
47
|
+
if command.respond_to?(:to_ary)
|
48
|
+
@command = sanitize(command[0])
|
49
|
+
args.unshift(*command[1..-1])
|
50
|
+
else
|
51
|
+
@command = sanitize(command)
|
52
|
+
end
|
53
|
+
@argv = args.map { |i| Shellwords.escape(i) }
|
54
|
+
end
|
55
|
+
@env ||= {}
|
56
|
+
@options = opts
|
57
|
+
|
58
|
+
@uuid = SecureRandom.uuid.split('-')[0]
|
59
|
+
@only_output_on_error = opts.fetch(:only_output_on_error) { false }
|
60
|
+
freeze
|
61
|
+
end
|
62
|
+
|
63
|
+
# Extend command options if keys don't already exist
|
64
|
+
#
|
65
|
+
# @api public
|
66
|
+
def update(options)
|
67
|
+
@options.update(options.update(@options))
|
68
|
+
end
|
69
|
+
|
70
|
+
# The shell environment variables
|
71
|
+
#
|
72
|
+
# @api public
|
73
|
+
def environment
|
74
|
+
@env.merge(options.fetch(:env, {}))
|
75
|
+
end
|
76
|
+
|
77
|
+
def environment_string
|
78
|
+
environment.map do |key, val|
|
79
|
+
converted_key = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s
|
80
|
+
escaped_val = val.to_s.gsub(/"/, '\"')
|
81
|
+
%(#{converted_key}="#{escaped_val}")
|
82
|
+
end.join(' ')
|
83
|
+
end
|
84
|
+
|
85
|
+
def evars(value, &block)
|
86
|
+
return (value || block) unless environment.any?
|
87
|
+
"( export #{environment_string} ; #{value || block.call} )"
|
88
|
+
end
|
89
|
+
|
90
|
+
def umask(value)
|
91
|
+
return value unless options[:umask]
|
92
|
+
%(umask #{options[:umask]} && %s) % [value]
|
93
|
+
end
|
94
|
+
|
95
|
+
def chdir(value)
|
96
|
+
return value unless options[:chdir]
|
97
|
+
%(cd #{Shellwords.escape(options[:chdir])} && #{value})
|
98
|
+
end
|
99
|
+
|
100
|
+
def user(value)
|
101
|
+
return value unless options[:user]
|
102
|
+
vars = environment.any? ? "#{environment_string} " : ''
|
103
|
+
%(sudo -u #{options[:user]} #{vars}-- sh -c '%s') % [value]
|
104
|
+
end
|
105
|
+
|
106
|
+
def group(value)
|
107
|
+
return value unless options[:group]
|
108
|
+
%(sg #{options[:group]} -c \\\"%s\\\") % [value]
|
109
|
+
end
|
110
|
+
|
111
|
+
# Clear environment variables except specified by env
|
112
|
+
#
|
113
|
+
# @api public
|
114
|
+
def with_clean_env
|
115
|
+
end
|
116
|
+
|
117
|
+
# Assemble full command
|
118
|
+
#
|
119
|
+
# @api public
|
120
|
+
def to_command
|
121
|
+
chdir(umask(evars(user(group(to_s)))))
|
122
|
+
end
|
123
|
+
|
124
|
+
# @api public
|
125
|
+
def to_s
|
126
|
+
[command.to_s, *Array(argv)].join(' ')
|
127
|
+
end
|
128
|
+
|
129
|
+
# @api public
|
130
|
+
def to_hash
|
131
|
+
{
|
132
|
+
command: command,
|
133
|
+
argv: argv,
|
134
|
+
uuid: uuid
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
private
|
139
|
+
|
140
|
+
# Coerce to string
|
141
|
+
#
|
142
|
+
# @api private
|
143
|
+
def sanitize(value)
|
144
|
+
value.to_s.dup
|
145
|
+
end
|
146
|
+
end # Cmd
|
147
|
+
end # Command
|
148
|
+
end # TTY
|