class Cmds
Definitions
¶ ↑
debug logging stuff
Definitions
¶ ↑
Constants
- DEFAULTS
hash of common default values used in method options.
don't use them directly – use {Cmds.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.- QUOTE_TYPES
Quote “name” keys `:single` and `:double` mapped to their character.
@return [Hash<Symbol, String>]
- QUOTE_VALUES
List containing just `'` and `“`.
@return [Array<String>]
- ROOT
Absolute, expanded path to the gem's root directory.
@return [Pathname]
- TOKENIZE_OPT_KEYS
- VERSION
Library version string.
@return [String]
Attributes
base/common positional parameters to render into the command template.
defaults to `[]`.
{#prepare} and the methods that invoke it (like {#capture}, {#stream}, etc.) accept `*args`, which will be appended to these values to create the final array for rendering.
@return [Array<Object>]
if `true`, will execution will raise an error on non-zero exit code.
defaults to `false`.
@return [Boolean]
Optional directory to run the command in, set by the `:chdir` option in {Cmds#initialize}.
@return [nil]
If the command will not change directory to run (default behavior).
@return [String | Pathname]
If the command will change directory to run.
Environment variables to set for command execution.
defaults to `{}`.
@return [Hash{String | Symbol => String}]
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`
@return [:inline, :spawn_arg]
format specifier symbol:
-
`:squish`
-
collapse rendered command string to one line.
-
-
`:pretty`
-
clean up and backslash suffix line endings.
-
defaults to `:squish`.
@return [:squish | :pretty]
string or readable IO-like object to use as default input to the command.
{#prepare} and the methods that invoke it (like {#capture}, {#stream}, etc.) accept an optional block that will override this value if present.
@return [String | read]
base/common keyword parameters to render into the command template.
defaults to `{}`.
{#prepare} and the methods that invoke it (like {#capture}, {#stream}, etc.) accept `**kwds`, which will be merged on top of these values to create the final hash for rendering.
@return [Hash{Symbol => Object}]
The results of the last time {Cmds#prepare} was called on the instance.
A little bit funky, I know, but it turns out to be quite useful.
@return [nil]
If {Cmds#prepare} has never been called.
@return [String]
If {Cmds#prepare} has been called.
ERB stirng template (with Cmds-specific extensions) for the command.
@return [String]
Public Class Methods
create a new {Cmds} and
# 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
create a new {Cmds} from template with parameters and call {Cmds#capture} on it.
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare)
@param [#call] &input_block
optional block that returns a string or IO-like readable object to be used as input for the execution.
@return [Result]
result with command string, exist status, stdout and stderr.
# File lib/cmds/sugar.rb, line 65 def self.capture template, *args, **kwds, &input_block Cmds.new(template).capture *args, **kwds, &input_block end
raise an error unless the exit status is 0.
@param [String] cmd
the command sting that was executed.
@param [Fixnum] status
the command's exit status.
@return [nil]
@raise [SystemCallError]
if exit status is not 0.
# 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
captures a new {Cmds}, captures and chomps stdout (sugar for `Cmds.out(template, *args, **kwds, &input_block).chomp`).
@see .out
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &input_block (see .capture)
@return [String]
the command's chomped stdout.
# File lib/cmds/sugar.rb, line 159 def self.chomp template, *args, **kwds, &input_block out(template, *args, **kwds, &input_block).chomp end
captures and chomps stdout, raising an error if the command fails. (sugar for `Cmds.out!(template, *args, **kwds, &input_block).chomp`).
@see .out!
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &input_block (see .capture)
@return [String]
the command's chomped stdout.
@raise [SystemCallError]
if the command fails (non-zero exit status).
# File lib/cmds/sugar.rb, line 180 def self.chomp! template, *args, **kwds, &input_block out!(template, *args, **kwds, &input_block).chomp end
log a debug message along with an optional hash of values.
# 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
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.
@param [Hash] opts
hash of overrides provided by method caller.
@param [Array<Symbol>, '*'] keys
keys for the defaults you want to use.
@param [Hash<Symbol, Object>] extras
extra keys and values to add to the returned defaults.
@return [Hash<Symbol, Object>]
defaults to use in the method call.
# File lib/cmds/util/defaults.rb, line 106 def self.defaults opts, keys = '*', extras = {} if keys == '*' DEFAULTS.deep_dup else keys. map {|key| [key, DEFAULTS.fetch(key)] }. to_h end. merge!( extras ). merge!( opts ) end
captures and returns stderr (sugar for `Cmds.capture(template, *args, **kwds, &input_block).err`).
@see .capture
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &input_block (see .capture)
@return [String]
the command's stderr.
# File lib/cmds/sugar.rb, line 198 def self.err template, *args, **kwds, &input_block capture(template, *args, **kwds, &input_block).err end
# File lib/cmds/sugar.rb, line 86 def self.error? template, *args, **kwds, &io_block Cmds.new(template).error? *args, **kwds, &io_block end
Shortcut for Shellwords.escape
Also makes it easier to change or customize or whatever.
@see ruby-doc.org/stdlib/libdoc/shellwords/rdoc/Shellwords.html#method-c-escape
@param [#to_s] str @return [String]
# File lib/cmds/util/shell_escape.rb, line 38 def self.esc str Shellwords.escape str end
Formats a command string.
@param [String] string
Command string to format.
@param [nil, :squish, :pretty, call
] with
How to format the command string.
# 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
Construct a `Cmds` instance.
@param [String] template
String template to use when creating the command string to send to the shell via {#prepare}. Allows ERB (positional and keyword), `%s` (positional) and `%{name}` (keyword) placeholders. Available as the {#template} attribute.
@param [Array<Object>] args:
Positional arguments to interpolate into the template on {#prepare}. Available as the {#args} attribute.
@param [Boolean] assert:
When `true`, execution will raise an error if the command doesn't exit successfully (if the command exits with any status other than `0`). Available as the {#assert} attribute.
@param [nil | String | Pathname] chdir:
Optional directory to change into when executing. Available as the {#chdir} attribute.
@param [Hash{(String | Symbol) => String}] env:
Hash of environment variables to set when executing the command. Available as the {#env} attribute.
@param [:inline, :spawn_arg] env_mode
:
Controls how the env vars are added to the command. - `:inline` adds them to the top of the prepared string. This is nice if you want do print the command out and paste it into a terminal. This is the default. - `:spawn_arg` passes them as an argument to `Process.spawn`. In this case they will not be included in the output of {#prepare} (or {#render}). Available as the {#env_mode} attribute.
@param [nil, :squish, :pretty, call
] format:
Dictates how to format the rendered template string before passing off to the shell. This feature lets you write templates in a more relaxed manner without `\` line-endings all over the place. - `nil` performs **no formatting at all*. - `:squish` reduces any consecutive whitespace (including newlines) to a single space. This is the default. - `:pretty` tries to keep the general formatting but make it acceptable to the shell by adding `\` at the end of lines. See {Cmds.pretty_format}. - An object that responds to `#call` will be called with the command string as it's only argument for custom formatting. See {Cmds.format} for more details. Available as the {#format} attribute.
@param [nil | String | read] input:
Input to send to the command on execution. Can be a string or an `IO`-like object that responds to `#read`. Available as the {#input} attribute.
@param [Hash{Symbol => Object}] kwds:
Keyword arguments to shell escape and interpolate into the template on {#prepare}. Available as the {#kwds} attribute.
# File lib/cmds.rb, line 237 def initialize template, **options options = defaults options if options.key? :opts options[:kwds][:opts] = options.delete :opts end logger.trace "Cmd constructing...", template: template, options: options @template = template # Assign options to instance variables options.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
create a new {Cmds} from template with parameters and call {Cmd#ok?} on it.
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &io_block (see Cmds.spawn
)
@return [Result]
result with command string, exist status, stdout and stderr.
# File lib/cmds/sugar.rb, line 81 def self.ok? template, *args, **kwds, &io_block Cmds.new(template).ok? *args, **kwds, &io_block end
creates a new {Cmds}, captures and returns stdout (sugar for `Cmds.capture(template, *args, **kwds, &input_block).out`).
@see Cmd.out
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &input_block (see .capture)
@return [String]
the command's stdout.
# File lib/cmds/sugar.rb, line 120 def self.out template, *args, **kwds, &input_block Cmds.new(template).out *args, **kwds, &input_block end
creates a new {Cmds}, captures and returns stdout. raises an error if the command fails.
@see Cmd.out!
@param template (see .prepare) @param *args (see .prepare) @param **kwds (see .prepare) @param &input_block (see .capture)
@return [String]
the command's stdout.
@raise [SystemCallError]
if the command fails (non-zero exit status).
# File lib/cmds/sugar.rb, line 141 def self.out! template, *args, **kwds, &input_block Cmds.new(template).out! *args, **kwds, &input_block end
create a new {Cmds} instance with the template and parameters and calls {Cmds#prepare}.
@param [String] template
ERB template parameters are rendered into to create the command string.
@param [Array<Object>] *args
positional parameters for rendering into the template.
@param [Hash{Symbol => Object}] **kwds
keyword parameters for rendering into the template.
@return [String]
rendered and formatted command string ready to be executed.
# File lib/cmds/sugar.rb, line 40 def self.prepare template, *args, **kwds, &options_block options = if options_block options_block.call else {} end Cmds.new(template, **options).prepare *args, **kwds end
# 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
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.
@example Single quoting string containing single quotes
Cmds.quote_dance %{you're}, :single # => %{'you'"'"'re'}
@example Double quoting string containing double quotes
Cmds.quote_dance %{hey "ho" let's go}, :double # => %{"hey "'"'"ho"'"'" let's go"}
**_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_**
@param [String] string
String to quote.
@return [return_type]
@todo Document return value.
# 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
# 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 a string for use in the shell.
@param [String] string
String to quote.
@return [String]
Single-quoted string.
# File lib/cmds/util/shell_escape.rb, line 93 def self.single_quote string quote_dance string, :single end
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
nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html
with major modifications from looking at Ruby's [open3][] module.
[open3]: ruby-doc.org/stdlib/libdoc/open3/rdoc/Open3.html
At the end of the day ends up calling `Process.spawn`.
@param [String] cmd
**SHELL-READY** command string. This is important - whatever you feed in here will be run **AS IS** - no escaping, formatting, etc.
@param [Hash{(Symbol | String) => Object}] env
Hash of `ENV` vars to provide for the command. We convert symbol keys to strings, but other than that just pass it through to `Process.spawn`, which I think will `#to_s` everything. Pretty much you want to have everything be strings or symbols for this to make any sense but we're not checking shit at the moment. If the {Cmds#env_mode} is `:inline` it should have already prefixed `cmd` with the definitions and not provide this keyword (or provide `{}`).
@param [nil | String | read] input
String or readable input, or `nil` (meaning no input). Allows {Cmds} instances can pass their `@input` instance variable. Don't provide input here and via `io_block`.
@param [Hash<Symbol, Object>] **spawn_opts
Any additional options are passed as the [options][Process.spawn options] to {Process.spawn} [Process.spawn options]: http://ruby-doc.org/core/Process.html#method-c-spawn
@param [#call & (arity ∈ {0, 1})] &io_block
Optional block to handle io. Behavior depends on arity: - Arity `0` - Block is called and expected to return an object suitable for input (`nil`, `String` or `IO`-like). - Arity `1` - Block is called with the {Cmds::IOHandler} instance for the execution, which it can use to handle input and outputs. Don't provide input here and via `input` keyword arg.
@return [Fixnum]
Command exit status.
@raise [ArgumentError]
If `&io_block` has arity greater than 1.
@raise [ArgumentError]
If input is provided via the `input` keyword arg and the `io_block`.
# File lib/cmds/spawn.rb, line 116 def self.spawn cmd, env: {}, input: nil, **spawn_opts, &io_block logger.trace "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 logger.trace "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 logger.trace "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 logger.trace "input should be io-like, setting spawn opt.", input: input if input == $stdin logger.trace "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| logger.trace "looking at #{ name }..." dest = handler.public_send sym # see if hanlder.out or hanlder.err is a Proc if dest.is_a? Proc logger.trace "#{ 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 logger.trace "#{ name } should be io-like, setting spawn opt.", output: dest spawn_opts[sym] = dest # the pipe is nil! nil end end # map outputs logger.trace "spawning...", env: env, cmd: cmd, opts: spawn_opts pid = Process.spawn env.map {|k, v| [k.to_s, v]}.to_h, cmd, spawn_opts logger.trace "spawned.", pid: pid wait_thread = Process.detach pid wait_thread[:name] = "WAIT" logger.trace "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 logger.trace "thread started, writing input..." in_pipe.w.write input unless input.nil? logger.trace "write done, closing in_pipe.w..." in_pipe.w.close logger.trace "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 logger.trace "thread started" loop do logger.trace "blocking on gets..." line = pipe.r.gets if line.nil? logger.trace "received nil, output done." else logger.trace \ "received #{ line.bytesize } bytes, passing to handler." end handler.thread_send_line pipe.sym, line break if line.nil? end logger.trace \ "reading done, closing pipe.r (unless already closed)..." pipe.r.close unless pipe.r.closed? logger.trace "thread done." end # thread end # if pipe end # map threads logger.trace "handing off main thread control to the handler..." begin handler.start logger.trace "handler done." ensure # wait for the threads to complete logger.trace "joining threads..." [in_thread, out_thread, err_thread, wait_thread].each do |thread| if thread logger.trace "joining #{ thread[:name] } thread..." thread.join end end logger.trace "all threads done." end status = wait_thread.value.exitstatus logger.trace "exit status: #{ status.inspect }" logger.trace "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 logger.trace "streaming completed." return status end
# File lib/cmds/sugar.rb, line 97 def self.stream template, *subs, &input_block Cmds.new(template).stream *subs, &input_block end
# 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 for the shell. each values is tokenized individually and the results are joined with a space.
@param [Array<Object>] *values
values to tokenize.
@return [String]
tokenized string ready for the shell.
# File lib/cmds/util.rb, line 26 def self.tokenize *values, **opts values.map {|value| case value when Hash tokenize_options value, **opts else tokenize_value value, **opts end }.flatten.join ' ' end
Turn an option name and value into an array of shell-escaped string tokens suitable for use in a command.
@param [String] name
String name (one or more characters).
@param [*] value
Value of the option.
@param [Hash] **opts
@option [Symbol] :array_mode (:join)
one of: 1. `:join` (default) -- join values in one token. tokenize_option 'blah', [1, 2, 3], array_mode: :join => ['--blah=1,2,3'] 2. `:repeat` repeat the option for each value. tokenize_option 'blah', [1, 2, 3], array_mode: :repeat => ['--blah=1', '--blah=2', '--blah=3']
@option [String] :array_join_string (',')
String to join array values with when `:array_mode` is `:join`.
@return [Array<String>]
List of individual shell token strings, escaped for use.
@raise [ArgumentError]
1. If `name` is the wrong type or empty. 2. If any options have bad values.
# 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 name = name.gsub( '_', '-' ) if opts[:dash_opt_names] # 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
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.
# File lib/cmds/util/tokenize_options.rb, line 32 def self.tokenize_options 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
turn an option name and value into an array of shell-escaped string token suitable for use in a command.
@param [String] name
string name (one or more characters).
@param [*] value
value of the option.
@param [Hash] **opts @option [Symbol] :array_mode (:multiple)
one of: 1. `:multiple` (default) provide one token for each value. expand_option 'blah', [1, 2, 3] => ['--blah=1', '--blah=2', '--blah=3'] 2. `:join` -- join values in one token. expand_option 'blah', [1, 2, 3], array_mode: :join => ['--blah=1,2,3']
@option [String] :array_join_string (',')
string to join array values with when `:array_mode` is `:join`.
@return [Array<String>]
List of individual shell token strings.
@raise [ArgumentError]
If options are set to bad values.
# File lib/cmds/util/tokenize_value.rb, line 60 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
Public Instance Methods
executes the command and returns a {Cmds::Result} with the captured outputs.
@param [Array<Object>] *args
positional parameters to append to those in `@args` for rendering into the command string.
@param [Hash{Symbol => Object}] **kwds
keyword parameters that override those in `@kwds` for rendering into the command string.
@param [#call] &input_block
optional block that returns a string or readable object to override `@input`.
@return [Cmds::Result]
result of execution with command string, status, stdout and stderr.
# File lib/cmds/capture.rb, line 21 def capture *args, **kwds, &input_block logger.trace "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 logger.trace "configured input", input: input # strings output will be concatenated onto out = '' err = '' logger.trace "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 logger.trace "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
captures and chomps stdout (sugar for `#out(*subs, &input_block).chomp`).
@see out
@param *args (see capture
) @param **kwds (see capture
) @param &input_block (see capture
)
@return [String]
the command's chomped stdout.
# File lib/cmds.rb, line 427 def chomp *args, **kwds, &input_block out(*args, **kwds, &input_block).chomp end
captures and chomps stdout, raising an error if the command failed. (sugar for `#out!(*subs, &input_block).chomp`).
@see capture
@see Result#out
@param *args (see capture
) @param **kwds (see capture
) @param &input_block (see capture
)
@return [String]
the command's chomped stdout.
@raise [SystemCallError]
if the command fails (non-zero exit status).
# File lib/cmds.rb, line 448 def chomp! *args, **kwds, &input out!(*args, **kwds, &input).chomp end
returns a new {Cmds} with the parameters and input merged in
# File lib/cmds.rb, line 269 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
proxy through to class method {Cmds.defaults}.
# File lib/cmds/util/defaults.rb, line 123 def defaults opts, keys = '*', extras = {} self.class.defaults opts, keys, extras end
captures and returns stdout (sugar for `#capture(*subs, &input_block).err`).
@param *args (see capture
) @param **kwds (see capture
) @param &input_block (see capture
)
@see capture
@see Result#err
@return [String]
the command's stderr.
# File lib/cmds.rb, line 466 def err *args, **kwds, &input_block capture(*args, **kwds, &input_block).err end
execute command and return `true` if it exited successfully.
@param *args (see capture
) @param **kwds (see capture
) @param &input_block (see capture
)
@return [Boolean]
`true` if exit code was `0`.
# File lib/cmds.rb, line 353 def ok? *args, **kwds, &io_block stream(*args, **kwds, &io_block) == 0 end
captures and returns stdout (sugar for `#capture(*args, **kwds, &input_block).out`).
@see capture
@see Result#out
@param *args (see capture
) @param **kwds (see capture
) @param &input_block (see capture
)
@return [String]
the command's stdout.
# File lib/cmds.rb, line 392 def out *args, **kwds, &input_block capture(*args, **kwds, &input_block).out end
captures and returns stdout (sugar for `#capture(*args, **kwds, &input_block).out`).
@see capture
@see Result#out
@param args [Array] see {.capture}. @param kwds [Proc] see {.capture}.
@return [String] the command's stdout.
@raise [SystemCallError] if the command fails (non-zero exit status).
# File lib/cmds.rb, line 410 def out! *args, **kwds, &input capture(*args, **kwds, &input).assert.out end
# File lib/cmds.rb, line 372 def proxy stream do |io| io.in = $stdin end end
render parameters into `@template`.
@note the returned string is not formatted for shell execution.
Cmds passes this string through {Cmds.format} before execution, which addresses newlines in the rendered string through "squishing" everything down to one line or adding `\` to line ends.
@param args (see capture
) @param kwds (see capture
)
@return [String]
the rendered command string.
# File lib/cmds.rb, line 295 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 a command.
@param *args (see capture
) @param **kwds (see capture
)
@param [nil | String | read] &io_block
string or readable IO-like object to use as input to the command.
@return [Fixnum]
command exit status.
# File lib/cmds/stream.rb, line 13 def stream *args, **kwds, &io_block logger.trace "entering Cmds#stream", args: args, kwds: kwds, io_block: io_block spawn *args, **kwds, &io_block end
stream and raise an error if exit code is not 0.
@param *args (see capture
) @param **kwds (see capture
) @param &io_block (see stream
) @return [Fixnum] (see stream
)
@raise [SystemCallError]
if exit status is not 0.
# 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
Protected Instance Methods
Internal method that simply passes through to {Cmds.spawn}, serving as a hook point for subclasses.
Accepts and returns the same things as {Cmds#stream}.
@param (see Cmds#stream
) @return (see Cmds#stream
)
# File lib/cmds/spawn.rb, line 367 def spawn *args, **kwds, &io_block Cmds.spawn prepare(*args, **kwds), input: input, # include env if mode is spawn argument env: (env_mode == :spawn_arg ? env : {}), chdir: chdir, unsetenv_others: !!@unsetenv_others, &io_block end