Class: Cmds
- Inherits:
-
Object
- Object
- Cmds
- Defined in:
- lib/cmds.rb,
lib/cmds/pipe.rb,
lib/cmds/util.rb,
lib/cmds/debug.rb,
lib/cmds/spawn.rb,
lib/cmds/sugar.rb,
lib/cmds/result.rb,
lib/cmds/stream.rb,
lib/cmds/capture.rb,
lib/cmds/version.rb,
lib/cmds/io_handler.rb,
lib/cmds/erb_context.rb,
lib/cmds/shell_eruby.rb,
lib/cmds/util/params.rb,
lib/cmds/util/defaults.rb,
lib/cmds/util/shell_escape.rb,
lib/cmds/util/tokenize_value.rb,
lib/cmds/util/tokenize_option.rb,
lib/cmds/util/tokenize_options.rb
Overview
Definitions
Defined Under Namespace
Modules: Debug Classes: ERBContext, IOHandler, Params, Pipe, Result, ShellEruby
Shell Quoting and Escaping Methods collapse
- QUOTE_TYPES =
Quote "name" keys
:single
and:double
mapped to their character. { single: %{'}, double: %{"}, }.freeze
- QUOTE_VALUES =
List containing just
'
and"
. QUOTE_TYPES.values.freeze
Constant Summary collapse
- ROOT =
Absolute, expanded path to the gem's root directory.
(Pathname.new( __FILE__ ).dirname / '..' / '..').
- VERSION =
Library version string.
"0.2.7"
- DEFAULTS =
hash of common default values used in method options.
don't use them directly -- use defaults.
the values themselves are frozen so we don't have to worry about cloning them before providing them for use.
the constant Hash itself is not frozen -- you can mutate this to change the default options for ALL Cmds method calls... just be aware of what you're doing. not recommended outside of quick hacks and small scripts since other pieces and parts you don't even know about may depend on said behavior.
{ # Alphabetical... # positional arguments for a command args: [], # what to join array option values with when using `array_mode = :join` array_join_string: ',', # what to do with array option values array_mode: :join, # Don't asset (raise error if exit code is not 0) assert: false, # Don't change directories chdir: nil, # No additional environment env: {}, # Stick ENV var defs inline at beginning of command env_mode: :inline, # What to do with `false` *option* values (not `false` values as regular # values or inside collections) # # Just leave them out all-together false_mode: :omit, # Flatten nested array values to a single array. # # Many CLI commands accept arrays in some form or another, but I'm hard # pressed to think of one that accepts nested arrays. Flattening can make # it simpler to generate values. # flatten_array_values: true, # how to format a command string for execution format: :squish, hash_mode: :join, # Join hash keys and values with `:` hash_join_string: ':', # No input input: nil, # keyword arguments for a command kwds: {}, # What to use to separate "long" opt names (more than one character) from # their values. I've commonly seen '=' (`--name=VALUE`) # and ' ' (`--name VALUE`). long_opt_separator: '=', # What to use to separate "short" opt names (single character) from their # values. I've commonly seen ' ' (`-x VALUE`) and '' (`-xVALUE`). short_opt_separator: ' ', }.map { |k, v| [k, v.freeze] }.to_h.freeze
- TOKENIZE_OPT_KEYS =
[ :array_mode, :array_join_string, :false_mode, :flatten_array_values, :hash_mode, :hash_join_string, :long_opt_separator, :short_opt_separator, ].freeze
Instance Attribute Summary collapse
-
#args ⇒ Array<Object>
readonly
base/common positional parameters to render into the command template.
-
#assert ⇒ Boolean
readonly
if
true
, will execution will raise an error on non-zero exit code. -
#chdir ⇒ nil, String | Pathname
readonly
Optional directory to run the command in, set by the
:chdir
option in #initialize. -
#env ⇒ Hash{String | Symbol => String}
readonly
Environment variables to set for command execution.
-
#env_mode ⇒ :inline, :spawn_arg
readonly
How environment variables will be set for command execution - inline at the top of the command, or passed to
Process.spawn
as an argument. -
#format ⇒ :squish | :pretty
readonly
format specifier symbol:.
-
#input ⇒ String | #read
readonly
string or readable IO-like object to use as default input to the command.
-
#kwds ⇒ Hash{Symbol => Object}
readonly
base/common keyword parameters to render into the command template.
-
#last_prepared_cmd ⇒ nil, String
readonly
The results of the last time #prepare was called on the instance.
-
#template ⇒ String
readonly
ERB stirng template (with Cmds-specific extensions) for the command.
Execution Instance Methods collapse
-
#chomp(*args, **kwds, &input_block) ⇒ String
captures and chomps stdout (sugar for
#out(*subs, &input_block).chomp
). -
#chomp!(*args, **kwds, &input) ⇒ String
captures and chomps stdout, raising an error if the command failed.
-
#err(*args, **kwds, &input_block) ⇒ String
captures and returns stdout (sugar for
#capture(*subs, &input_block).err
). -
#error?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return
true
if it failed. -
#ok?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return
true
if it exited successfully. -
#out(*args, **kwds, &input_block) ⇒ String
captures and returns stdout (sugar for
#capture(*args, **kwds, &input_block).out
). -
#out!(*args, **kwds, &input) ⇒ String
captures and returns stdout (sugar for
#capture(*args, **kwds, &input_block).out
). - #proxy ⇒ Object
Spawn Methods collapse
-
.spawn(cmd, env: {}, input: nil, **spawn_opts, &io_block) ⇒ Fixnum
Low-level static method to spawn and stream inputs and/or outputs using threads.
Shell Quoting and Escaping Methods collapse
-
.esc(str) ⇒ String
Shortcut for Shellwords.escape.
-
.quote_dance(string, quote_type) ⇒ return_type
Format a string to be a shell token by wrapping it in either single or double quotes and replacing instances of that quote with what I'm calling a "quote dance":.
-
.single_quote(string) ⇒ String
Single quote a string for use in the shell.
Class Method Summary collapse
-
.assert(template, *args, **kwds, &io_block) ⇒ Object
create a new Cmds and.
- .capture(template, *args, **kwds, &input_block) ⇒ Result
-
.check_status(cmd, status, err = nil) ⇒ nil
raise an error unless the exit status is 0.
-
.chomp(template, *args, **kwds, &input_block) ⇒ String
captures a new Cmds, captures and chomps stdout (sugar for
Cmds.out(template, *args, **kwds, &input_block).chomp
). -
.chomp!(template, *args, **kwds, &input_block) ⇒ String
captures and chomps stdout, raising an error if the command fails.
-
.debug(msg, values = {}) ⇒ Object
log a debug message along with an optional hash of values.
-
.defaults(opts, keys = '*', extras = {}) ⇒ Hash<Symbol, Object>
merge an method call options hash with common defaults for the module.
-
.err(template, *args, **kwds, &input_block) ⇒ String
captures and returns stderr (sugar for
Cmds.capture(template, *args, **kwds, &input_block).err
). - .error?(template, *args, **kwds, &io_block) ⇒ Boolean
-
.format(string, with = :squish) ⇒ Object
Formats a command string.
-
.ok?(template, *args, **kwds, &io_block) ⇒ Result
create a new Cmds from template with parameters and call Cmd#ok? on it.
-
.out(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout (sugar for
Cmds.capture(template, *args, **kwds, &input_block).out
). -
.out!(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout.
- .prepare(template, *args, **kwds, &options_block) ⇒ String
- .pretty_format(string) ⇒ Object
- .replace_shortcuts(template) ⇒ Object
- .stream(template, *subs, &input_block) ⇒ Object
- .stream!(template, *args, **kwds, &io_block) ⇒ Object
-
.tokenize(*values, **opts) ⇒ String
tokenize values for the shell.
-
.tokenize_option(name, value, **opts) ⇒ Array<String>
Turn an option name and value into an array of shell-escaped string tokens suitable for use in a command.
-
.tokenize_options(hash, **opts) ⇒ Object
escape option hash.
-
.tokenize_value(value, **opts) ⇒ Array<String>
turn an option name and value into an array of shell-escaped string token suitable for use in a command.
Instance Method Summary collapse
-
#capture(*args, **kwds, &input_block) ⇒ Cmds::Result
(also: #call)
executes the command and returns a Result with the captured outputs.
-
#curry(*args, **kwds, &input_block) ⇒ Object
returns a new Cmds with the parameters and input merged in.
-
#defaults(opts, keys = '*', extras = {}) ⇒ Object
proxy through to class method Cmds.defaults.
-
#initialize(template, **opts) ⇒ Cmds
constructor
Construct a
Cmds
instance. -
#prepare(*args, **kwds) ⇒ String
prepare a shell-safe command string for execution.
-
#render(*args, **kwds) ⇒ String
render parameters into
@template
. -
#stream(*args, **kwds, &io_block) ⇒ Fixnum
stream a command.
-
#stream!(*args, **kwds, &io_block) ⇒ Object
stream and raise an error if exit code is not 0.
Constructor Details
#initialize(template, **opts) ⇒ Cmds
Construct a Cmds
instance.
229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 |
# File 'lib/cmds.rb', line 229 def initialize template, **opts opts = defaults opts Cmds.debug "Cmd constructing...", template: template, opts: opts @template = template # Assign options to instance variables opts.each { |key, value| instance_variable_set "@#{ key }", value } # An internal cache of the last result of calling {#prepare}, or `nil` if # {#prepare} has never been called. Kinda funky but ends up being useful. @last_prepared_cmd = nil end |
Instance Attribute Details
#args ⇒ Array<Object> (readonly)
52 53 54 |
# File 'lib/cmds.rb', line 52 def args @args end |
#assert ⇒ Boolean (readonly)
if true
, will execution will raise an error on non-zero exit code.
defaults to false
.
86 87 88 |
# File 'lib/cmds.rb', line 86 def assert @assert end |
#chdir ⇒ nil, String | Pathname (readonly)
Optional directory to run the command in, set by the :chdir
option
in #initialize.
130 131 132 |
# File 'lib/cmds.rb', line 130 def chdir @chdir end |
#env ⇒ Hash{String | Symbol => String} (readonly)
Environment variables to set for command execution.
defaults to {}
.
95 96 97 |
# File 'lib/cmds.rb', line 95 def env @env end |
#env_mode ⇒ :inline, :spawn_arg (readonly)
How environment variables will be set for command execution - inline at
the top of the command, or passed to Process.spawn
as an argument.
See the inline
105 106 107 |
# File 'lib/cmds.rb', line 105 def env_mode @env_mode end |
#format ⇒ :squish | :pretty (readonly)
format specifier symbol:
-
:squish
- collapse rendered command string to one line.
-
:pretty
- clean up and backslash suffix line endings.
defaults to :squish
.
118 119 120 |
# File 'lib/cmds.rb', line 118 def format @format end |
#input ⇒ String | #read (readonly)
77 78 79 |
# File 'lib/cmds.rb', line 77 def input @input end |
#kwds ⇒ Hash{Symbol => Object} (readonly)
65 66 67 |
# File 'lib/cmds.rb', line 65 def kwds @kwds end |
#last_prepared_cmd ⇒ nil, String (readonly)
The results of the last time #prepare was called on the instance.
A little bit funky, I know, but it turns out to be quite useful.
143 144 145 |
# File 'lib/cmds.rb', line 143 def last_prepared_cmd @last_prepared_cmd end |
#template ⇒ String (readonly)
ERB stirng template (with Cmds-specific extensions) for the command.
38 39 40 |
# File 'lib/cmds.rb', line 38 def template @template end |
Class Method Details
.assert(template, *args, **kwds, &io_block) ⇒ Object
create a new Cmds and
92 93 94 |
# File 'lib/cmds/sugar.rb', line 92 def self.assert template, *args, **kwds, &io_block Cmds.new(template).capture(*args, **kwds, &io_block).assert end |
.capture(template, *args, **kwds, &input_block) ⇒ Result
65 66 67 |
# File 'lib/cmds/sugar.rb', line 65 def self.capture template, *args, **kwds, &input_block Cmds.new(template).capture *args, **kwds, &input_block end |
.check_status(cmd, status, err = nil) ⇒ nil
raise an error unless the exit status is 0.
128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 |
# File 'lib/cmds/util.rb', line 128 def self.check_status cmd, status, err = nil unless status.equal? 0 msg = NRSER.squish <<-END command `#{ cmd }` exited with status #{ status } END if err msg += " and stderr:\n\n" + err end # Remove NULL bytes (not sure how they get in there...) msg = msg.delete("\000") raise SystemCallError.new msg, status end end |
.chomp(template, *args, **kwds, &input_block) ⇒ String
captures a new Cmds, captures and chomps stdout
(sugar for Cmds.out(template, *args, **kwds, &input_block).chomp
).
159 160 161 |
# File 'lib/cmds/sugar.rb', line 159 def self.chomp template, *args, **kwds, &input_block out(template, *args, **kwds, &input_block).chomp end |
.chomp!(template, *args, **kwds, &input_block) ⇒ String
captures and chomps stdout, raising an error if the command fails.
(sugar for Cmds.out!(template, *args, **kwds, &input_block).chomp
).
180 181 182 |
# File 'lib/cmds/sugar.rb', line 180 def self.chomp! template, *args, **kwds, &input_block out!(template, *args, **kwds, &input_block).chomp end |
.debug(msg, values = {}) ⇒ Object
log a debug message along with an optional hash of values.
96 97 98 99 100 |
# File 'lib/cmds/debug.rb', line 96 def self.debug msg, values = {} # don't even bother unless debug logging is turned on return unless Debug.on? Debug.logger.debug Debug.format(msg, values) end |
.defaults(opts, keys = '*', extras = {}) ⇒ Hash<Symbol, Object>
merge an method call options hash with common defaults for the module.
this makes it easy to use the same defaults in many different methods without repeating the declarations everywhere.
98 99 100 101 102 103 104 105 106 107 108 109 110 |
# File 'lib/cmds/util/defaults.rb', line 98 def self.defaults opts, keys = '*', extras = {} if keys == '*' DEFAULTS.dup else keys. map {|key| [key, DEFAULTS.fetch(key)] }. to_h end. merge!( extras ). merge!( opts ) end |
.err(template, *args, **kwds, &input_block) ⇒ String
captures and returns stderr
(sugar for Cmds.capture(template, *args, **kwds, &input_block).err
).
198 199 200 |
# File 'lib/cmds/sugar.rb', line 198 def self.err template, *args, **kwds, &input_block capture(template, *args, **kwds, &input_block).err end |
.error?(template, *args, **kwds, &io_block) ⇒ Boolean
86 87 88 |
# File 'lib/cmds/sugar.rb', line 86 def self.error? template, *args, **kwds, &io_block Cmds.new(template).error? *args, **kwds, &io_block end |
.esc(str) ⇒ String
Shortcut for Shellwords.escape
Also makes it easier to change or customize or whatever.
38 39 40 |
# File 'lib/cmds/util/shell_escape.rb', line 38 def self.esc str Shellwords.escape str end |
.format(string, with = :squish) ⇒ Object
Formats a command string.
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/cmds/util.rb', line 46 def self.format string, with = :squish case with when nil string when :squish NRSER.squish string when :pretty pretty_format string else with.call string end end |
.ok?(template, *args, **kwds, &io_block) ⇒ Result
create a new Cmds from template with parameters and call Cmd#ok? on it.
81 82 83 |
# File 'lib/cmds/sugar.rb', line 81 def self.ok? template, *args, **kwds, &io_block Cmds.new(template).ok? *args, **kwds, &io_block end |
.out(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout
(sugar for Cmds.capture(template, *args, **kwds, &input_block).out
).
120 121 122 |
# File 'lib/cmds/sugar.rb', line 120 def self.out template, *args, **kwds, &input_block Cmds.new(template).out *args, **kwds, &input_block end |
.out!(template, *args, **kwds, &input_block) ⇒ String
creates a new Cmds, captures and returns stdout. raises an error if the command fails.
141 142 143 |
# File 'lib/cmds/sugar.rb', line 141 def self.out! template, *args, **kwds, &input_block Cmds.new(template).out! *args, **kwds, &input_block end |
.prepare(template, *args, **kwds, &options_block) ⇒ String
40 41 42 43 44 45 46 47 48 |
# File 'lib/cmds/sugar.rb', line 40 def self.prepare template, *args, **kwds, & = if .call else {} end Cmds.new(template, **).prepare *args, **kwds end |
.pretty_format(string) ⇒ Object
63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
# File 'lib/cmds/util.rb', line 63 def self.pretty_format string string = string.gsub(/\n(\s*\n)+\n/, "\n\n") string.lines.map {|line| line = line.rstrip if line.end_with? '\\' line elsif line == '' '\\' elsif line =~ /\s$/ line + '\\' else line + ' \\' end }.join("\n") end |
.quote_dance(string, quote_type) ⇒ return_type
Format a string to be a shell token by wrapping it in either single or double quotes and replacing instances of that quote with what I'm calling a "quote dance":
- Closing the type of quote in use
- Quoting the type of quote in use with the other type of quote
- Then opening up the type in use again and keeping going.
WARNING: Does NOT escape anything except the quotes! So if you double-quote a string with shell-expansion terms in it and pass it to the shell THEY WILL BE EVALUATED
72 73 74 75 76 77 78 79 80 81 82 |
# File 'lib/cmds/util/shell_escape.rb', line 72 def self.quote_dance string, quote_type outside = QUOTE_TYPES.fetch quote_type inside = QUOTE_VALUES[QUOTE_VALUES[0] == outside ? 1 : 0] outside + string.gsub( outside, outside + inside + outside + inside + outside ) + outside end |
.replace_shortcuts(template) ⇒ Object
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/cmds/util.rb', line 81 def self.replace_shortcuts template template .gsub( # %s => <%= arg %> /(?<=\A|\=|[[:space:]])\%s(?=\Z|[[:space:]])/, '<%= arg %>' ) .gsub( # %%s => %s (escaping) /(?<=\A|[[:space:]])(\%+)\%s(?=\Z|[[:space:]])/, '\1s' ) .gsub( # %{key} => <%= key %>, %{key?} => <%= key? %> /(?<=\A|\=|[[:space:]])\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/, '<%= \1 %>' ) .gsub( # %%{key} => %{key}, %%{key?} => %{key?} (escaping) /(?<=\A|[[:space:]])(\%+)\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/, '\1{\2}\3' ) .gsub( # %<key>s => <%= key %>, %<key?>s => <%= key? %> /(?<=\A|\=|[[:space:]])\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/, '<%= \1 %>' ) .gsub( # %%<key>s => %<key>s, %%<key?>s => %<key?>s (escaping) /(?<=\A|[[:space:]])(\%+)\%\<([a-zA-Z_]+\??)\>s(?=\Z|[[:space:]])/, '\1<\2>s' ) end |
.single_quote(string) ⇒ String
Single quote a string for use in the shell.
93 94 95 |
# File 'lib/cmds/util/shell_escape.rb', line 93 def self.single_quote string quote_dance string, :single end |
.spawn(cmd, env: {}, input: nil, **spawn_opts, &io_block) ⇒ Fixnum
Low-level static method to spawn and stream inputs and/or outputs using threads.
This is the core execution functionality of the whole library - everything ends up here.
WARNING - This method runs the cmd
string AS IS - no escaping,
formatting, interpolation, etc. are done at this point.
The whole rest of the library is built on top of this method to provide that stuff, and if you're using this library, you probably want to use that stuff.
You should not need to use this method directly unless you are extending the library's functionality.
Originally inspired by
https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
with major modifications from looking at Ruby's open3 module.
At the end of the day ends up calling Process.spawn
.
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 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 203 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 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
# File 'lib/cmds/spawn.rb', line 116 def self.spawn cmd, env: {}, input: nil, **spawn_opts, &io_block Cmds.debug "entering Cmds#spawn", cmd: cmd, env: env, input: input, spawn_opts: spawn_opts, io_block: io_block # Process.spawn doesn't like a `nil` chdir if spawn_opts.key?( :chdir ) && spawn_opts[:chdir].nil? spawn_opts.delete :chdir end # create the handler that will be yielded to the input block handler = Cmds::IOHandler.new # handle input # # if a block was provided it overrides the `input` argument. # if io_block case io_block.arity when 0 # when the input block takes no arguments it returns the input # Check that `:input` kwd wasn't provided. unless input.nil? raise ArgumentError, "Don't call Cmds.spawn with `:input` keyword arg and a block" end input = io_block.call when 1 # when the input block takes one argument, give it the handler and # ignore the return value io_block.call handler # if input was assigned to the handler in the block, use it as input unless handler.in.nil? # Check that `:input` kwd wasn't provided. unless input.nil? raise ArgumentError, "Don't call Cmds.spawn with `:input` keyword arg and a block" end input = handler.in end else # bad block provided raise ArgumentError.new NRSER.squish <<-BLOCK provided input block must have arity 0 or 1 BLOCK end # case io_block.arity end # if io_block Cmds.debug "looking at input...", input: input # (possibly) create the input pipe... this will be nil if the provided # input is io-like. in this case it will be used directly in the # `spawn` options. in_pipe = case input when nil, String Cmds.debug "input is a String or nil, creating pipe..." in_pipe = Cmds::Pipe.new "INPUT", :in spawn_opts[:in] = in_pipe.r # don't buffer input in_pipe.w.sync = true in_pipe else Cmds.debug "input should be io-like, setting spawn opt.", input: input if input == $stdin Cmds.debug "input is $stdin." end spawn_opts[:in] = input nil end # case input # (possibly) create the output pipes. # # `stream` can be told to send it's output to either: # # 1. a Proc that will invoked with each line. # 2. an io-like object that can be provided as `spawn`'s `:out` or # `:err` options. # # in case (1) a `Cmds::Pipe` wrapping read and write piped `IO` instances # will be created and assigned to the relevant of `out_pipe` or `err_pipe`. # # in case (2) the io-like object will be sent directly to `spawn` and # the relevant `out_pipe` or `err_pipe` will be `nil`. # out_pipe, err_pipe = [ ["ERROR", :err], ["OUTPUT", :out], ].map do |name, sym| Cmds.debug "looking at #{ name }..." dest = handler.public_send sym # see if hanlder.out or hanlder.err is a Proc if dest.is_a? Proc Cmds.debug "#{ name } is a Proc, creating pipe..." pipe = Cmds::Pipe.new name, sym # the corresponding :out or :err option for spawn needs to be # the pipe's write handle spawn_opts[sym] = pipe.w # return the pipe pipe else Cmds.debug "#{ name } should be io-like, setting spawn opt.", output: dest spawn_opts[sym] = dest # the pipe is nil! nil end end # map outputs Cmds.debug "spawning...", env: env, cmd: cmd, opts: spawn_opts pid = Process.spawn env.map {|k, v| [k.to_s, v]}.to_h, cmd, spawn_opts Cmds.debug "spawned.", pid: pid wait_thread = Process.detach pid wait_thread[:name] = "WAIT" Cmds.debug "wait thread created.", thread: wait_thread # close child ios if created # the spawned process will read from in_pipe.r so we don't need it in_pipe.r.close if in_pipe # and we don't need to write to the output pipes, that will also happen # in the spawned process [out_pipe, err_pipe].each {|pipe| pipe.w.close if pipe} # create threads to handle any pipes that were created in_thread = if in_pipe Thread.new do Thread.current[:name] = in_pipe.name Cmds.debug "thread started, writing input..." in_pipe.w.write input unless input.nil? Cmds.debug "write done, closing in_pipe.w..." in_pipe.w.close Cmds.debug "thread done." end # Thread end out_thread, err_thread = [out_pipe, err_pipe].map do |pipe| if pipe Thread.new do Thread.current[:name] = pipe.name Cmds.debug "thread started" loop do Cmds.debug "blocking on gets..." line = pipe.r.gets if line.nil? Cmds.debug "received nil, output done." else Cmds.debug NRSER.squish <<-BLOCK received #{ line.bytesize } bytes, passing to handler. BLOCK end handler.thread_send_line pipe.sym, line break if line.nil? end Cmds.debug "reading done, closing pipe.r (unless already closed)..." pipe.r.close unless pipe.r.closed? Cmds.debug "thread done." end # thread end # if pipe end # map threads Cmds.debug "handing off main thread control to the handler..." begin handler.start Cmds.debug "handler done." ensure # wait for the threads to complete Cmds.debug "joining threads..." [in_thread, out_thread, err_thread, wait_thread].each do |thread| if thread Cmds.debug "joining #{ thread[:name] } thread..." thread.join end end Cmds.debug "all threads done." end status = wait_thread.value.exitstatus Cmds.debug "exit status: #{ status.inspect }" Cmds.debug "checking @assert and exit status..." if @assert && status != 0 # we don't necessarily have the err output, so we can't include it # in the error message msg = NRSER.squish <<-BLOCK streamed command `#{ cmd }` exited with status #{ status } BLOCK raise SystemCallError.new msg, status end Cmds.debug "streaming completed." return status end |
.stream(template, *subs, &input_block) ⇒ Object
97 98 99 |
# File 'lib/cmds/sugar.rb', line 97 def self.stream template, *subs, &input_block Cmds.new(template).stream *subs, &input_block end |
.stream!(template, *args, **kwds, &io_block) ⇒ Object
102 103 104 |
# File 'lib/cmds/sugar.rb', line 102 def self.stream! template, *args, **kwds, &io_block Cmds.new(template).stream! *args, **kwds, &io_block end |
.tokenize(*values, **opts) ⇒ String
tokenize values for the shell. each values is tokenized individually and the results are joined with a space.
26 27 28 29 30 31 32 33 34 35 |
# File 'lib/cmds/util.rb', line 26 def self.tokenize *values, **opts values.map {|value| case value when Hash value, **opts else tokenize_value value, **opts end }.flatten.join ' ' end |
.tokenize_option(name, value, **opts) ⇒ Array<String>
Turn an option name and value into an array of shell-escaped string tokens suitable for use in a command.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 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 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 |
# File 'lib/cmds/util/tokenize_option.rb', line 43 def self.tokenize_option name, value, **opts # Set defaults for any options not passed opts = defaults opts, TOKENIZE_OPT_KEYS # Validate `name` unless name.is_a?(String) && name.length > 0 raise ArgumentError.new NRSER.squish <<-END `name` must be a String of length greater than zero, found #{ name.inspect } END end # Set type (`:short` or `:long`) prefix and name/value separator depending # on if name is "short" (single character) or "long" (anything else) # type, prefix, separator = if name.length == 1 # -b <value> style (short) [ :short, '-', opts[:short_opt_separator] ] else # --blah=<value> style (long) [ :long, '--', opts[:long_opt_separator] ] end case value # Special cases (booleans), where we may want to emit an option name but # no value (depending on options) # when true # `-b` or `--blah` style token [prefix + esc(name)] when false case opts[:false_mode] when :omit, :ignore # Don't emit any token for a false boolean [] when :negate, :no # Emit `--no-blah` style token # if type == :long # Easy one ["--no-#{ esc(name) }"] else # Short option... there seems to be little general consensus on how # to handle these guys; I feel like the most common is to invert the # case, which only makes sense for languages that have lower and # upper case :/ case opts[:false_short_opt_mode] when :capitalize, :cap, :upper, :upcase # Capitalize the name # # {x: false} => ["-X"] # # This only really makes sense for lower case a-z, so raise if it's # not in there unless "a" <= name <= "z" raise ArgumentError.new binding.erb <<-END Can't negate CLI option `<%= name %>` by capitalizing name. Trying to tokenize option `<%= name %>` with `false` value and: 1. `:false_mode` is set to `<%= opts[:false_mode] %>`, which tells {Cmds.tokenize_option} to emit a "negating" name with no value like {update: false} => --no-update 2. `:false_short_opt_mode` is set to `<%= opts[:false_short_opt_mode] %>`, which means negate through capitalizing the name character, like: {u: false} => -U 3. But this is only implemented for names in `a-z` Either change the {Cmds} instance configuration or provide a different CLI option name or value. END end # Emit {x: false} => ['-X'] style ["-#{ name.upcase }"] when :long # Treat it the same as a long option, # emit {x: false} => ['--no-x'] style # # Yeah, I've never seen it anywhere else, but it seems reasonable I # guess..? # ["--no-#{ esc(name) }"] when :string # Use the string 'false' as a value [prefix + esc( name ) + separator + 'false'] when String # It's some custom string to use [prefix + esc( name ) + separator + esc( string )] else raise ArgumentError.new binding.erb <<-END Bad `:false_short_opt_mode` value: <%= opts[:false_short_opt_mode].pretty_inspect %> Should be 1. :capitalize (or :cap, :upper, :upcase) 2. :long 3. :string 4. any String END end # case opts[:false_short_opt_mode] end # if :long else else raise ArgumentError.new NRSER.squish <<-END bad :false_mode option: #{ opts[:false_mode] }, should be :omit or :no END end # General case else # Tokenize the value, which may # # 1. Result in more than one token, like when `:array_mode` is `:repeat` # (in which case we want to emit multiple option tokens) # # 2. Result in zero tokens, like when `value` is `nil` # (in which case we want to emit no option tokens) # # and map the resulting tokens into option tokens # tokenize_value( value, **opts ).map { |token| prefix + esc(name) + separator + token } end # case value end |
.tokenize_options(hash, **opts) ⇒ Object
escape option hash.
this is only useful for the two common option styles:
single character keys become
-<char> <value>
{x: 1} => "-x 1"
longer keys become
--<key>=<value>
options{blah: 2} => "--blah=2"
if you have something else, you're going to have to just put it in the cmd itself, like:
Cmds "blah -assholeOptionOn:%{s}", "ok"
or whatever similar shit said command requires.
however, if the value is an Array, it will repeat the option for each value:
{x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
{blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
i can't think of any right now, but i swear i've seen commands that take opts that way.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
# File 'lib/cmds/util/tokenize_options.rb', line 32 def self. hash, **opts opts = defaults opts, TOKENIZE_OPT_KEYS hash.map {|key, value| # keys need to be strings key = key.to_s unless key.is_a? String [key, value] }.sort {|(key_a, value_a), (key_b, value_b)| # sort by the (now string) keys key_a <=> key_b }.map {|key, value| tokenize_option key, value, **opts }.flatten.join ' ' end |
.tokenize_value(value, **opts) ⇒ Array<String>
turn an option name and value into an array of shell-escaped string token suitable for use in a command.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 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 |
# File 'lib/cmds/util/tokenize_value.rb', line 59 def self.tokenize_value value, **opts opts = defaults opts, TOKENIZE_OPT_KEYS case value when nil # `nil` values produces no tokens [] when Array # The PITA one... # # May produce one or multiple tokens. # # Flatten the array value if option is set value = value.flatten if opts[:flatten_array_values] case opts[:array_mode] when :repeat # Encode each entry as it's own token # # [1, 2, 3] => ["1", "2", "3"] # # Pass entries back through for individual tokenization and flatten # so we are sure to return a single-depth array value.map { |entry| tokenize_value entry, **opts }.flatten when :join # Encode all entries as one joined string token # # [1, 2, 3] => ["1,2,3"] # [esc( value.join opts[:array_join_string] )] when :json # Encode JSON dump as single token, single-quoted # # [1, 2, 3] => ["'[1,2,3]'"] [single_quote( JSON.dump value )] else # SOL raise ArgumentError.new binding.erb <<-END Bad `:array_mode` option: <%= opts[:array_mode].pretty_inspect %> Should be :join, :repeat or :json END end # case opts[:array_mode] when Hash # Much the same as array # # May produce one or multiple tokens. # case opts[:hash_mode] when :join # Join the key and value using the option and pass the resulting array # back through to be handled as configured tokenize_value \ value.map { |k, v| [k, v].join opts[:hash_join_string] }, **opts when :json # Encode JSON dump as single token, single-quoted # # [1, 2, 3] => [%{'{"a":1,"b":2,"c":3}'}] [single_quote( JSON.dump value )] else # SOL raise ArgumentError.new binding.erb <<-END Bad `:hash_mode` option: <%= opts[:hash_mode].pretty_inspect %> Should be :join, or :json END end else # We let {Cmds.esc} handle it, and return that as a single token [esc(value)] end end |
Instance Method Details
#capture(*args, **kwds, &input_block) ⇒ Cmds::Result Also known as: call
executes the command and returns a Result with the captured outputs.
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/cmds/capture.rb', line 21 def capture *args, **kwds, &input_block Cmds.debug "entering Cmds#capture", args: args, kwds: kwds, input: input # extract input from block via `call` if one is provided, # otherwise default to instance variable (which may be `nil`) input = input_block.nil? ? input : input_block.call Cmds.debug "configured input", input: input # strings output will be concatenated onto out = '' err = '' Cmds.debug "calling Cmds.spawn..." status = spawn(*args, **kwds) do |io| # send the input to stream, which sends it to spawn io.in = input # and concat the output lines as they come in io.on_out do |line| out += line end io.on_err do |line| err += line end end Cmds.debug "Cmds.spawn completed", status: status # build a Result # result = Cmds::Result.new cmd, status, out_reader.value, err_reader.value result = Cmds::Result.new last_prepared_cmd, status, out, err # tell the Result to assert if the Cmds has been told to, which will # raise a SystemCallError with the exit status if it was non-zero result.assert if assert return result end |
#chomp(*args, **kwds, &input_block) ⇒ String
captures and chomps stdout
(sugar for #out(*subs, &input_block).chomp
).
415 416 417 |
# File 'lib/cmds.rb', line 415 def chomp *args, **kwds, &input_block out(*args, **kwds, &input_block).chomp end |
#chomp!(*args, **kwds, &input) ⇒ String
captures and chomps stdout, raising an error if the command failed.
(sugar for #out!(*subs, &input_block).chomp
).
436 437 438 |
# File 'lib/cmds.rb', line 436 def chomp! *args, **kwds, &input out!(*args, **kwds, &input).chomp end |
#curry(*args, **kwds, &input_block) ⇒ Object
returns a new Cmds with the parameters and input merged in
257 258 259 260 261 262 263 264 265 266 267 |
# File 'lib/cmds.rb', line 257 def curry *args, **kwds, &input_block self.class.new template, { args: (self.args + args), kwds: (self.kwds.merge kwds), input: (input_block ? input_block.call : self.input), assert: self.assert, env: self.env, format: self.format, chdir: self.chdir, } end |
#defaults(opts, keys = '*', extras = {}) ⇒ Object
proxy through to class method defaults.
115 116 117 |
# File 'lib/cmds/util/defaults.rb', line 115 def defaults opts, keys = '*', extras = {} self.class.defaults opts, keys, extras end |
#err(*args, **kwds, &input_block) ⇒ String
captures and returns stdout
(sugar for #capture(*subs, &input_block).err
).
454 455 456 |
# File 'lib/cmds.rb', line 454 def err *args, **kwds, &input_block capture(*args, **kwds, &input_block).err end |
#error?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return true
if it failed.
355 356 357 |
# File 'lib/cmds.rb', line 355 def error? *args, **kwds, &io_block stream(*args, **kwds, &io_block) != 0 end |
#ok?(*args, **kwds, &io_block) ⇒ Boolean
execute command and return true
if it exited successfully.
341 342 343 |
# File 'lib/cmds.rb', line 341 def ok? *args, **kwds, &io_block stream(*args, **kwds, &io_block) == 0 end |
#out(*args, **kwds, &input_block) ⇒ String
captures and returns stdout
(sugar for #capture(*args, **kwds, &input_block).out
).
380 381 382 |
# File 'lib/cmds.rb', line 380 def out *args, **kwds, &input_block capture(*args, **kwds, &input_block).out end |
#out!(*args, **kwds, &input) ⇒ String
captures and returns stdout
(sugar for #capture(*args, **kwds, &input_block).out
).
398 399 400 |
# File 'lib/cmds.rb', line 398 def out! *args, **kwds, &input capture(*args, **kwds, &input).assert.out end |
#prepare(*args, **kwds) ⇒ String
prepare a shell-safe command string for execution.
321 322 323 |
# File 'lib/cmds.rb', line 321 def prepare *args, **kwds @last_prepared_cmd = Cmds.format render(*args, **kwds), self.format end |
#proxy ⇒ Object
360 361 362 363 364 |
# File 'lib/cmds.rb', line 360 def proxy stream do |io| io.in = $stdin end end |
#render(*args, **kwds) ⇒ String
the returned string is not formatted for shell execution.
Cmds passes this string through format before execution,
which addresses newlines in the rendered string through "squishing"
everything down to one line or adding \
to line ends.
render parameters into @template
.
283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 |
# File 'lib/cmds.rb', line 283 def render *args, **kwds # Create the context for ERB context = Cmds::ERBContext.new( (self.args + args), self.kwds.merge( kwds ), tokenize_options_opts: TOKENIZE_OPT_KEYS. each_with_object( {} ) { |key, hash| value = instance_variable_get "@#{ key}" hash[key] = value unless value.nil? } ) erb = Cmds::ShellEruby.new Cmds.replace_shortcuts( self.template ) rendered = NRSER.dedent erb.result(context.get_binding) if self.env_mode == :inline && !self.env.empty? rendered = self.env.sort_by {|name, value| name }.map {|name, value| "#{ name }=#{ Cmds.esc value }" }.join("\n\n") + "\n\n" + rendered end rendered end |
#stream(*args, **kwds, &io_block) ⇒ Fixnum
stream a command.
13 14 15 16 17 18 19 20 |
# File 'lib/cmds/stream.rb', line 13 def stream *args, **kwds, &io_block Cmds.debug "entering Cmds#stream", args: args, kwds: kwds, io_block: io_block spawn *args, **kwds, &io_block end |
#stream!(*args, **kwds, &io_block) ⇒ Object
stream and raise an error if exit code is not 0.
32 33 34 35 36 37 38 |
# File 'lib/cmds/stream.rb', line 32 def stream! *args, **kwds, &io_block status = stream *args, **kwds, &io_block Cmds.check_status last_prepared_cmd, status status end |