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
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 |
# File 'lib/dolzenko/shell_out.rb', line 97 def command(*args) (args.last.is_a?(Hash) ? args[0..-2] : args).join(" ") end |
.getopt(opt, default, *args) ⇒ Object
101 102 103 104 105 106 107 108 109 110 111 |
# File 'lib/dolzenko/shell_out.rb', line 101 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
116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 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 |
# File 'lib/dolzenko/shell_out.rb', line 116 def shell_out_with_pty(*args) old_state = `stty -g` return SUCCESS_EXIT_STATUS unless ShellOut::before(*args) # 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) PTY.spawn(ShellOut.command(*args)) do |r_pty, w_pty, 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.sysread(512) 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 rescue PTY::ChildExited => e return ShellOut::after(e.status.exitstatus, out_stream, *args) ensure system "stty #{ old_state }" end |
.shell_out_with_system(*args) ⇒ Object
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 203 204 205 206 207 208 |
# File 'lib/dolzenko/shell_out.rb', line 170 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 |