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,10 @@
1
+ # encoding: utf-8
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ FileList['tasks/**/*.rake'].each(&method(:import))
6
+
7
+ task default: :spec
8
+
9
+ desc 'Run all specs'
10
+ task ci: %w[ spec ]
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'tty-command'
5
+ require 'irb'
6
+ IRB.start
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,12 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ f = 'file'
8
+ if cmd.test("[ -f #{f} ]")
9
+ puts "#{f} already exists!"
10
+ else
11
+ cmd.run :touch, f
12
+ end
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ out, err = cmd.execute(:echo, 'hello world!')
8
+
9
+ puts "Result: #{out}"
@@ -0,0 +1,9 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ out, err = cmd.run(:echo, "$FOO", env: { foo: 'hello'})
8
+
9
+ puts "Result: #{out}"
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ require 'logger'
4
+ require 'tty-command'
5
+
6
+ logger = Logger.new('dev.log')
7
+
8
+ cmd = TTY::Command.new(output: logger, color: false)
9
+
10
+ cmd.run(:ls)
@@ -0,0 +1,10 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ out, err = cmd.run("echo 'hello' 1>& 2")
8
+
9
+ puts "out: #{out}"
10
+ puts "err: #{err}"
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ cmd.run(:ls, :out => 'ls_sample')
@@ -0,0 +1,7 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tty-command'
4
+
5
+ cmd = TTY::Command.new
6
+
7
+ cmd.run("while test 1; do echo 'hello'; sleep 1; done", timeout: 5)
@@ -0,0 +1 @@
1
+ require 'tty/command'
@@ -0,0 +1,160 @@
1
+ # encoding: utf-8
2
+
3
+ require 'thread'
4
+ require 'tty/command/version'
5
+ require 'tty/command/cmd'
6
+ require 'tty/command/exit_error'
7
+ require 'tty/command/dry_runner'
8
+ require 'tty/command/process_runner'
9
+ require 'tty/command/printers/null'
10
+ require 'tty/command/printers/pretty'
11
+ require 'tty/command/printers/progress'
12
+ require 'tty/command/printers/quiet'
13
+
14
+ module TTY
15
+ class Command
16
+ ExecuteError = Class.new(StandardError)
17
+
18
+ TimeoutExceeded = Class.new(StandardError)
19
+
20
+ attr_reader :printer
21
+
22
+ # Initialize a Command object
23
+ #
24
+ # @param [Hash] options
25
+ # @option options [IO] :output
26
+ # the stream to which printer prints, defaults to stdout
27
+ # @option options [Symbol] :printer
28
+ # the printer to use for output logging, defaults to :pretty
29
+ # @option options [Symbol] :dry_run
30
+ # the mode for executing command
31
+ #
32
+ # @api public
33
+ def initialize(options = {})
34
+ @output = options.fetch(:output) { $stdout }
35
+ @color = options.fetch(:color) { true }
36
+ @uuid = options.fetch(:uuid) { true }
37
+ @printer_name = options.fetch(:printer) { :pretty }
38
+ @dry_run = options.fetch(:dry_run) { false }
39
+ @printer = use_printer(@printer_name, color: @color, uuid: @uuid)
40
+ end
41
+
42
+ # Start external executable in a child process
43
+ #
44
+ # @example
45
+ # cmd.run(command, [argv1, ..., argvN], [options])
46
+ #
47
+ # @param [String] command
48
+ # the command to run
49
+ #
50
+ # @param [Array[String]] argv
51
+ # an array of string arguments
52
+ #
53
+ # @param [Hash] options
54
+ # hash of operations to perform
55
+ #
56
+ # @option options [String] :chdir
57
+ # The current directory.
58
+ #
59
+ # @option options [Integer] :timeout
60
+ # Maximum number of seconds to allow the process
61
+ # to run before aborting with a TimeoutExceeded
62
+ # exception.
63
+ #
64
+ # @raise [ExitError]
65
+ # raised when command exits with non-zero code
66
+ #
67
+ # @api public
68
+ def run(*args)
69
+ cmd = command(*args)
70
+ yield(cmd) if block_given?
71
+ result = execute_command(cmd)
72
+ if result && result.failure?
73
+ raise ExitError.new(cmd.to_command, result)
74
+ end
75
+ result
76
+ end
77
+
78
+ # Start external executable without raising ExitError
79
+ #
80
+ # @example
81
+ # cmd.run!(command, [argv1, ..., argvN], [options])
82
+ #
83
+ # @api public
84
+ def run!(*args)
85
+ cmd = command(*args)
86
+ yield(cmd) if block_given?
87
+ execute_command(cmd)
88
+ end
89
+
90
+ # Execute shell test command
91
+ #
92
+ # @api public
93
+ def test(*args)
94
+ run!(:test, *args).success?
95
+ end
96
+
97
+ # Check if in dry mode
98
+ #
99
+ # @return [Boolean]
100
+ #
101
+ # @public
102
+ def dry_run?
103
+ @dry_run
104
+ end
105
+
106
+ def printer
107
+ use_printer(@printer_name, color: @color, uuid: @uuid)
108
+ end
109
+
110
+ private
111
+
112
+ # @api private
113
+ def command(*args)
114
+ Cmd.new(*args)
115
+ end
116
+
117
+ # @api private
118
+ def execute_command(cmd)
119
+ mutex = Mutex.new
120
+ dry_run = @dry_run || cmd.options[:dry_run] || false
121
+ @runner = select_runner(@printer, dry_run)
122
+ mutex.synchronize { @runner.run(cmd) }
123
+ end
124
+
125
+ # @api private
126
+ def use_printer(class_or_name, options)
127
+ if class_or_name.is_a?(TTY::Command::Printers::Abstract)
128
+ return class_or_name
129
+ end
130
+
131
+ if class_or_name.is_a?(Class)
132
+ class_or_name
133
+ else
134
+ find_printer_class(class_or_name)
135
+ end.new(@output, options)
136
+ end
137
+
138
+ # Find printer class or fail
139
+ #
140
+ # @raise [ArgumentError]
141
+ #
142
+ # @api private
143
+ def find_printer_class(name)
144
+ const_name = name.to_s.capitalize.to_sym
145
+ if const_name.empty? || !TTY::Command::Printers.const_defined?(const_name)
146
+ fail ArgumentError, %(Unknown printer type "#{name}")
147
+ end
148
+ TTY::Command::Printers.const_get(const_name)
149
+ end
150
+
151
+ # @api private
152
+ def select_runner(printer, dry_run)
153
+ if dry_run
154
+ DryRunner.new(printer)
155
+ else
156
+ ProcessRunner.new(printer)
157
+ end
158
+ end
159
+ end # Command
160
+ end # TTY
@@ -0,0 +1,153 @@
1
+ # encoding: utf-8
2
+
3
+ require 'securerandom'
4
+
5
+ module TTY
6
+ class Command
7
+ class Cmd
8
+ # A string command name, or shell program
9
+ # @api public
10
+ attr_reader :command
11
+
12
+ # A string arguments
13
+ # @api public
14
+ attr_reader :argv
15
+
16
+ # Hash of operations to peform
17
+ # @api public
18
+ attr_reader :options
19
+
20
+ # Initialize a new Cmd object
21
+ #
22
+ # @api private
23
+ def initialize(env_or_cmd, *args)
24
+ opts = args.last.respond_to?(:to_hash) ? args.pop : {}
25
+ if env_or_cmd.respond_to?(:to_hash)
26
+ @env = env_or_cmd
27
+ unless command = args.shift
28
+ raise ArgumentError, 'Cmd requires command argument'
29
+ end
30
+ else
31
+ command = env_or_cmd
32
+ end
33
+
34
+ if args.empty? && cmd = command.to_s
35
+ raise ArgumentError, 'No command provided' if cmd.empty?
36
+ @command = sanitize(cmd)
37
+ @argv = []
38
+ else
39
+ if command.respond_to?(:to_ary)
40
+ @command = sanitize(command[0])
41
+ args.unshift(*command[1..-1])
42
+ else
43
+ @command = sanitize(command)
44
+ end
45
+ @argv = args.map { |i| shell_escape(i) }
46
+ end
47
+ @env ||= {}
48
+ @options = opts
49
+ end
50
+
51
+ # Unique identifier
52
+ #
53
+ # @api public
54
+ def uuid
55
+ @uuid ||= SecureRandom.uuid.split('-')[0]
56
+ end
57
+
58
+ # The shell environment variables
59
+ #
60
+ # @api public
61
+ def environment
62
+ @env.merge(options.fetch(:env, {}))
63
+ end
64
+
65
+ def environment_string
66
+ environment.map do |key, val|
67
+ converted_key = key.is_a?(Symbol) ? key.to_s.upcase : key.to_s
68
+ escaped_val = val.to_s.gsub(/"/, '\"')
69
+ %(#{converted_key}="#{escaped_val}")
70
+ end.join(' ')
71
+ end
72
+
73
+ def evars(value, &block)
74
+ return (value || block) unless environment.any?
75
+ %(( export #{environment_string} ; %s )) % [value || block.call]
76
+ end
77
+
78
+ def umask(value)
79
+ return value unless options[:umask]
80
+ %(umask #{options[:umask]} && %s) % [value]
81
+ end
82
+
83
+ def chdir(value)
84
+ return value unless options[:chdir]
85
+ %(cd #{options[:chdir]} && %s) % [value]
86
+ end
87
+
88
+ def user(value)
89
+ return value unless options[:user]
90
+ vars = environment.any? ? "#{environment_string} " : ''
91
+ %(sudo -u #{options[:user]} #{vars}-- sh -c '%s') % [value]
92
+ end
93
+
94
+ def group(value)
95
+ return value unless options[:group]
96
+ %(sg #{options[:group]} -c \\\"%s\\\") % [value]
97
+ end
98
+
99
+ # Clear environment variables except specified by env
100
+ #
101
+ # @api public
102
+ def with_clean_env
103
+ end
104
+
105
+ # Assemble full command
106
+ #
107
+ # @api public
108
+ def to_command
109
+ chdir(umask(evars(user(group(to_s)))))
110
+ end
111
+
112
+ # @api public
113
+ def to_s
114
+ [command.to_s, *Array(argv)].join(' ')
115
+ end
116
+
117
+ # @api public
118
+ def to_hash
119
+ {
120
+ command: command,
121
+ argv: argv,
122
+ uuid: uuid
123
+ }
124
+ end
125
+
126
+ private
127
+
128
+ # @api private
129
+ def sanitize(value)
130
+ cmd = value.to_s.dup
131
+ lines = cmd.lines.to_a
132
+ lines.each_with_index.reduce('') do |acc, (line, index)|
133
+ acc << line.strip
134
+ acc << '; ' if (index + 1) != lines.size
135
+ acc
136
+ end
137
+ end
138
+
139
+ # Enclose s in quotes if it contains characters that require escaping
140
+ #
141
+ # @param [String] arg
142
+ # the argument to escape
143
+ #
144
+ # @api private
145
+ def shell_escape(arg)
146
+ str = arg.to_s.dup
147
+ return str if str =~ /^[0-9A-Za-z+,.\/:=@_-]+$/
148
+ str.gsub!("'", "'\\''")
149
+ "'#{str}'"
150
+ end
151
+ end # Cmd
152
+ end # Command
153
+ end # TTY
@@ -0,0 +1,19 @@
1
+ # encoding: utf-8
2
+
3
+ module TTY
4
+ class Command
5
+ class DryRunner
6
+ def initialize(printer)
7
+ @printer = printer
8
+ end
9
+
10
+ def run(cmd)
11
+ cmd.to_command
12
+ message = "#{@printer.decorate('(dry run)', :blue)} "
13
+ message << @printer.decorate(cmd.to_command, :yellow, :bold)
14
+ @printer.write(message, cmd.uuid)
15
+ Result.new(0, '', '')
16
+ end
17
+ end # DryRunner
18
+ end # Command
19
+ end # TTY
@@ -0,0 +1,134 @@
1
+ # encoding: utf-8
2
+
3
+ require 'tempfile'
4
+ require 'securerandom'
5
+
6
+ module TTY
7
+ class Command
8
+ module Execute
9
+ extend self
10
+
11
+ # Execute command in a child process
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 execute
19
+ #
20
+ # @return [pid, stdin, stdout, stderr]
21
+ #
22
+ # @api public
23
+ def spawn(cmd)
24
+ @process_options = normalize_redirect_options(cmd.options)
25
+
26
+ # Create pipes
27
+ in_rd, in_wr = IO.pipe # reading
28
+ out_rd, out_wr = IO.pipe # writing
29
+ err_rd, err_wr = IO.pipe # error
30
+
31
+ # redirect fds
32
+ opts = ({
33
+ :in => in_rd, # in_wr => :close,
34
+ :out => out_wr,# out_rd => :close,
35
+ :err => err_wr,# err_rd => :close
36
+ }).merge(@process_options)
37
+
38
+ pid = Process.spawn(cmd.to_command, opts)
39
+
40
+ # close in parent process
41
+ [out_wr, err_wr].each { |fd| fd.close if fd }
42
+
43
+ tuple = [pid, in_wr, out_rd, err_rd]
44
+
45
+ if block_given?
46
+ begin
47
+ return yield(*tuple)
48
+ ensure
49
+ [in_wr, out_rd, err_rd].each { |fd| fd.close if fd && !fd.closed? }
50
+ end
51
+ else
52
+ tuple
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def normalize_redirect_options(options)
59
+ options.reduce({}) do |opts, (key, value)|
60
+ if fd?(key)
61
+ process_key = fd_to_process_key(key)
62
+ if process_key.to_s == 'in'
63
+ value = convert_to_fd(value)
64
+ end
65
+ opts[process_key]= value
66
+ end
67
+ opts
68
+ end
69
+ end
70
+
71
+ # @api private
72
+ def fd?(object)
73
+ case object
74
+ when :stdin, :stdout, :stderr, :in, :out, :err
75
+ true
76
+ when STDIN, STDOUT, STDERR, $stdin, $stdout, $stderr, ::IO
77
+ true
78
+ when ::IO
79
+ true
80
+ when ::Fixnum
81
+ object >= 0
82
+ when respond_to?(:to_i) && !object.to_io.nil?
83
+ true
84
+ else
85
+ false
86
+ end
87
+ end
88
+
89
+ def try_reading(object)
90
+ if object.respond_to?(:read)
91
+ object.read
92
+ elsif object.respond_to?(:to_s)
93
+ object.to_s
94
+ else
95
+ object
96
+ end
97
+ end
98
+
99
+ def convert_to_fd(object)
100
+ return object if fd?(object)
101
+
102
+ if object.is_a?(::String) && File.exists?(object)
103
+ return object
104
+ end
105
+
106
+ tmp = Tempfile.new(SecureRandom.uuid.split('-')[0])
107
+
108
+ content = try_reading(object)
109
+ tmp.write(content)
110
+ tmp.rewind
111
+ tmp
112
+ end
113
+
114
+ def fd_to_process_key(object)
115
+ case object
116
+ when STDIN, $stdin, :in, :stdin, 0
117
+ :in
118
+ when STDOUT, $stdout, :out, :stdout, 1
119
+ :out
120
+ when STDERR, $stderr, :err, :stderr, 2
121
+ :err
122
+ when Fixnum
123
+ object >= 0 ? IO.for_fd(object) : nil
124
+ when IO
125
+ object
126
+ when respond_to?(:to_io)
127
+ object.to_io
128
+ else
129
+ raise ExecuteError, "Wrong execute redirect: #{object.inspect}"
130
+ end
131
+ end
132
+ end # Execute
133
+ end # Command
134
+ end # TTY