tty-command 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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