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

args[R]

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>]

assert[R]

if `true`, will execution will raise an error on non-zero exit code.

defaults to `false`.

@return [Boolean]

chdir[R]

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.
env[R]

Environment variables to set for command execution.

defaults to `{}`.

@return [Hash{String | Symbol => String}]

env_mode[R]

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[R]

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]

input[R]

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]

kwds[R]

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}]

last_prepared_cmd[R]

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.
template[R]

ERB stirng template (with Cmds-specific extensions) for the command.

@return [String]

Public Class Methods

assert(template, *args, **kwds, &io_block) click to toggle source

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
capture(template, *args, **kwds, &input_block) click to toggle source

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
check_status(cmd, status, err = nil) click to toggle source

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
chomp(template, *args, **kwds, &input_block) click to toggle source

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
chomp!(template, *args, **kwds, &input_block) click to toggle source

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
debug(msg, values = {}) click to toggle source

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
defaults(opts, keys = '*', extras = {}) click to toggle source

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
err(template, *args, **kwds, &input_block) click to toggle source

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
error?(template, *args, **kwds, &io_block) click to toggle source
# 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) click to toggle source

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
format(string, with = :squish) click to toggle source

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
new(template, **options) click to toggle source

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
ok?(template, *args, **kwds, &io_block) click to toggle source

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
out(template, *args, **kwds, &input_block) click to toggle source

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
out!(template, *args, **kwds, &input_block) click to toggle source

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
prepare(template, *args, **kwds, &options_block) click to toggle source

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
pretty_format(string) click to toggle source
# 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) click to toggle source

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”:

  1. Closing the type of quote in use

  2. Quoting the type of quote in use with the other type of quote

  3. 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
replace_shortcuts(template) click to toggle source
# 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) click to toggle source

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
spawn(cmd, env: {}) click to toggle source

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
stream(template, *subs, &input_block) click to toggle source
# 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) click to toggle source
# 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) click to toggle source

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
tokenize_option(name, value, **opts) click to toggle source

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
tokenize_options(hash, **opts) click to toggle source

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
tokenize_value(value, **opts) click to toggle source

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

call(*args, **kwds, &input_block)
Alias for: capture
capture(*args, **kwds, &input_block) click to toggle source

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
Also aliased as: call
chomp(*args, **kwds, &input_block) click to toggle source

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
chomp!(*args, **kwds, &input) click to toggle source

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
curry(*args, **kwds, &input_block) click to toggle source

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
defaults(opts, keys = '*', extras = {}) click to toggle source

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
err(*args, **kwds, &input_block) click to toggle source

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
error?(*args, **kwds, &io_block) click to toggle source

execute command and return `true` if it failed.

@param *args (see capture) @param **kwds (see capture) @param &input_block (see capture)

@return [Boolean]

`true` if exit code was not `0`.
# File lib/cmds.rb, line 367
def error? *args, **kwds, &io_block
  stream(*args, **kwds, &io_block) != 0
end
ok?(*args, **kwds, &io_block) click to toggle source

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
out(*args, **kwds, &input_block) click to toggle source

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
out!(*args, **kwds, &input) click to toggle source

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
prepare(*args, **kwds) click to toggle source

prepare a shell-safe command string for execution.

@param args (see capture) @param kwds (see capture)

@return [String]

the prepared command string.
# File lib/cmds.rb, line 333
def prepare *args, **kwds
  @last_prepared_cmd = Cmds.format render(*args, **kwds), self.format
end
proxy() click to toggle source
# File lib/cmds.rb, line 372
def proxy
  stream do |io|
    io.in = $stdin
  end
end
render(*args, **kwds) click to toggle source

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(*args, **kwds, &io_block) click to toggle source

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!(*args, **kwds, &io_block) click to toggle source

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

spawn(*args, **kwds, &io_block) click to toggle source

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