Module: ShellOut
- Defined in:
- lib/dolzenko/shell_out.rb
Overview
## ShellOut
Provides a convenient feature-rich way to “shell out” to external commands. Most useful features come from using ‘PTY` to execute the command. Not available on Windows, `Kernel#system` will be used instead.
## Features
### Interruption
The external command can be easily interrupted and ‘Interrupt` exception will propagate to the calling program.
For example while something like this can hang your terminal
loop { system("ls -R /") } # => lists directories indefinitely,
# Ctrl-C only stops ls
That won’t be the case with ShellOut:
require "shell_out"
include ShellOut
loop { shell_out("ls -R /") } # => when Ctrl-C is pressed ls is terminated
# and Interrupt exception is propagated
Yes it’s possible to examine the ‘$CHILD_STATUS.exitstatus` variable but that’s not nearly as robust and flexible as ‘PTY` solution.
### TTY-like Output
External command is running in pseudo TTY provided by ‘PTY` library on Unix, which means that commands like `ffmpeg`, `git` can report progress and **otherwise interact with user as usual**.
### Output Capturing
Output of the command can be captured using ‘:out` option
io = StringIO.new
shell_out("echo 42", :out => io) # doesn't print anything
io.string.chomp # => "42"
If ‘:out => :return` option is passed - the `shell_out` return the output of the command instead of exit status.
### :raise_exceptions, :verbose, :noop, and :dry_run Options
-
‘:raise_exceptions => true` will raise `ShellOutException` for any non-zero
exit status of the command
Following options have the same semantics as ‘FileUtils` method options do
-
‘:verbose => true` will echo command before execution
-
‘:noop => true` will just return zero exit status without executing the command
-
‘:dry_run => true` equivalent to `:verbose => true, :noop => true`
Defined Under Namespace
Classes: ShellOutException
Constant Summary collapse
- CTRL_C_CODE =
?\C-c
- SUCCESS_EXIT_STATUS =
0
Class Method Summary collapse
- .after(exitstatus, out_stream, *args) ⇒ Object
- .before(*args) ⇒ Object
- .command(*args) ⇒ Object
- .getopt(opt, default, *args) ⇒ Object
- .shell_out ⇒ Object
- .shell_out_with_pty(*args) ⇒ Object
- .shell_out_with_system(*args) ⇒ Object
- .with_env(*args) ⇒ Object
Class Method Details
.after(exitstatus, out_stream, *args) ⇒ Object
83 84 85 86 87 88 89 90 91 92 93 94 95 |
# File 'lib/dolzenko/shell_out.rb', line 83 def after(exitstatus, out_stream, *args) if args.last.is_a?(Hash) && args.last[:raise_exceptions] == true unless exitstatus == SUCCESS_EXIT_STATUS raise ShellOutException, "`#{ args[0..-2].join(" ") }' command finished with non-zero (#{ exitstatus }) exit status" end end if args.last.is_a?(Hash) && args.last[:out] == :return out_stream.rewind if out_stream.is_a?(StringIO) out_stream.read else exitstatus end end |
.before(*args) ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/dolzenko/shell_out.rb', line 68 def before(*args) if args.last.is_a?(Hash) = args.last verbose, dry_run, noop = .delete(:verbose), .delete(:dry_run), .delete(:noop) verbose = noop = true if dry_run puts "Executing: #{ args[0..-2].join(" ") }" if verbose return false if noop end true end |
.command(*args) ⇒ Object
97 98 99 100 101 102 |
# File 'lib/dolzenko/shell_out.rb', line 97 def command(*args) stripped_command = args.dup stripped_command.pop if stripped_command[-1].is_a?(Hash) # remove options stripped_command.shift if stripped_command[0].is_a?(Hash) # remove env stripped_command.join(" ") end |
.getopt(opt, default, *args) ⇒ Object
120 121 122 123 124 125 126 127 128 129 130 |
# File 'lib/dolzenko/shell_out.rb', line 120 def getopt(opt, default, *args) if args.last.is_a?(Hash) if opt == :out && args.last[:out] == :return StringIO.new else args.last.fetch(opt, default) end else default end end |
.shell_out ⇒ Object
.shell_out_with_pty(*args) ⇒ Object
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 |
# File 'lib/dolzenko/shell_out.rb', line 135 def shell_out_with_pty(*args) old_state = `stty -g` return SUCCESS_EXIT_STATUS unless ShellOut::before(*args) begin # stolen from ruby/ext/pty/script.rb # disable echoing and enable raw (not having to press enter) system "stty -echo raw lnext ^_" in_stream = ShellOut.getopt(:in, STDIN, *args) out_stream = ShellOut.getopt(:out, STDOUT, *args) writer = nil spawned_pid = nil ShellOut.with_env(*args) do PTY.spawn(ShellOut.command(*args)) do |r_pty, w_pty, pid| spawned_pid = pid reader = Thread.current writer = Thread.new do while true break if (ch = in_stream.getc).nil? ch = ch.chr if ch == ShellOut::CTRL_C_CODE reader.raise Interrupt, "Interrupted by user" else w_pty.print ch w_pty.flush end end end writer.abort_on_exception = true loop do c = begin r_pty.getc rescue Errno::EIO, EOFError nil end break if c.nil? out_stream.print c out_stream.flush end begin # try to invoke waitpid() before the signal handler does it return ShellOut::after(Process::waitpid2(pid)[1].exitstatus, out_stream, *args) rescue Errno::ECHILD # the signal handler managed to call waitpid() first; # PTY::ChildExited will be delivered pretty soon, so just wait for it sleep 1 end end end rescue PTY::ChildExited => e return ShellOut::after(e.status.exitstatus, out_stream, *args) ensure if writer writer.kill rescue nil end if spawned_pid Process.kill(-9, spawned_pid) rescue nil end system "stty #{ old_state }" end end |
.shell_out_with_system(*args) ⇒ Object
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 |
# File 'lib/dolzenko/shell_out.rb', line 204 def shell_out_with_system(*args) return SUCCESS_EXIT_STATUS unless ShellOut::before(*args) cleaned_args = if args.last.is_a?(Hash) = args.last.dup.delete_if { |k, | [ :verbose, :raise_exceptions ].include?(k) } require "stringio" if [:out].is_a?(StringIO) || [:out] == :return r, w = IO.pipe [:out] = w [:err] = [ :child, :out ] end if [:in].is_a?(StringIO) in_r, in_w = IO.pipe in_w.write [:in].read in_w.close [:in] = in_r end .empty? ? args[0..-2] : args[0..-2].dup << else args end exitstatus = if Kernel.system(*cleaned_args) SUCCESS_EXIT_STATUS else require "English" $CHILD_STATUS.exitstatus end if r w.close unless args.last[:out] == :return args.last[:out] << r.read end end ShellOut::after(exitstatus, r, *args) end |
.with_env(*args) ⇒ Object
104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 |
# File 'lib/dolzenko/shell_out.rb', line 104 def with_env(*args) yield unless (env = args[0]).is_a?(Hash) stored_env = {} for name, value in env stored_env[name] = ENV[name] value == nil ? ENV.delete(name) : ENV[name] = value end begin yield ensure for name, value in stored_env ENV[name] = value end end end |