Class: Cmds

Inherits:
Object
  • Object
show all
Defined in:
lib/cmds/debug.rb,
lib/cmds.rb,
lib/cmds/pipe.rb,
lib/cmds/util.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/tokenize_option.rb,
lib/cmds/util/tokenize_options.rb

Overview

debug logging stuff

Defined Under Namespace

Modules: Debug Classes: ERBContext, IOHandler, Params, Pipe, Result, ShellEruby

Constant Summary collapse

VERSION =
"0.2.3"
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.

{
  # positional arguments for a command
  args: [],
  
  # keyword arguments for a command
  kwds: {},
  
  # how to format a command string for execution
  format: :squish,
  
  # what to do with array option values
  array_mode: :join,
  
  # what to join array option values with when using `array_mode = :join`
  array_join_string: ',',
  
  # what to do with false array values
  false_mode: :omit,
  
  # Stick ENV var defs inline at beginning of command
  env_mode: :inline,
  
  # No additional environment
  env: {},
  
  # Don't change directories
  chdir: nil,
  
  # Don't asset (raise error if exit code is not 0)
  assert: false,
  
  # No input
  input: nil,
  
}.map { |k, v| [k, v.freeze] }.to_h.freeze
TOKENIZE_OPT_KEYS =
[:array_mode, :array_join_string, :false_mode]

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template, **opts) ⇒ Cmds

Construct a Cmds instance.

Parameters:

  • template (String)

    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.

  • args: (Array<Object>)

    Positional arguments to interpolate into the template on #prepare.

    Available as the #args attribute.

  • assert: (Boolean)

    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.

  • chdir: (nil | String | Pathname)

    Optional directory to change into when executing.

    Available as the #chdir attribute.

  • env: (Hash{(String | Symbol) => String})

    Hash of environment variables to set when executing the command.

    Available as the #env attribute.

  • env_mode: (:inline, :spawn_arg)

    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.

  • format: (nil, :squish, :pretty, #call)

    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 pretty_format.

    • An object that responds to #call will be called with the command string as it's only argument for custom formatting.

    See format for more details.

    Available as the #format attribute.

  • input: (nil | String | #read)

    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.

  • kwds: (Hash{Symbol => Object})

    Keyword arguments to shell escape and interpolate into the template on #prepare.

    Available as the #kwds attribute.



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
# File 'lib/cmds.rb', line 211

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

#argsArray<Object> (readonly)

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.

Returns:

  • (Array<Object>)


35
36
37
# File 'lib/cmds.rb', line 35

def args
  @args
end

#assertBoolean (readonly)

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

defaults to false.

Returns:

  • (Boolean)


69
70
71
# File 'lib/cmds.rb', line 69

def assert
  @assert
end

#chdirnil, String | Pathname (readonly)

Optional directory to run the command in, set by the :chdir option in #initialize.

Returns:

  • (nil)

    If the command will not change directory to run (default behavior).

  • (String | Pathname)

    If the command will change directory to run.



113
114
115
# File 'lib/cmds.rb', line 113

def chdir
  @chdir
end

#envHash{String | Symbol => String} (readonly)

Environment variables to set for command execution.

defaults to {}.

Returns:

  • (Hash{String | Symbol => String})


78
79
80
# File 'lib/cmds.rb', line 78

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

Returns:

  • (:inline, :spawn_arg)


88
89
90
# File 'lib/cmds.rb', line 88

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.

Returns:

  • (:squish | :pretty)


101
102
103
# File 'lib/cmds.rb', line 101

def format
  @format
end

#inputString | #read (readonly)

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.

Returns:

  • (String | #read)


60
61
62
# File 'lib/cmds.rb', line 60

def input
  @input
end

#kwdsHash{Symbol => Object} (readonly)

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.

Returns:

  • (Hash{Symbol => Object})


48
49
50
# File 'lib/cmds.rb', line 48

def kwds
  @kwds
end

#last_prepared_cmdnil, 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.

Returns:

  • (nil)

    If #prepare has never been called.

  • (String)

    If #prepare has been called.



127
128
129
# File 'lib/cmds.rb', line 127

def last_prepared_cmd
  @last_prepared_cmd
end

#templateString (readonly)

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

Returns:

  • (String)


21
22
23
# File 'lib/cmds.rb', line 21

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

create a new Cmds from template with parameters and call #capture on it.

Parameters:

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

Returns:

  • (Result)

    result with command string, exist status, stdout and stderr.



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.

Parameters:

  • cmd (String)

    the command sting that was executed.

  • status (Fixnum)

    the command's exit status.

Returns:

  • (nil)

Raises:

  • (SystemCallError)

    if exit status is not 0.



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/cmds/util.rb', line 137

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
    
    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).

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

Returns:

  • (String)

    the command's chomped stdout.

See Also:



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).

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

Returns:

  • (String)

    the command's chomped stdout.

Raises:

  • (SystemCallError)

    if the command fails (non-zero exit status).

See Also:



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.

Parameters:

  • opts (Hash)

    hash of overrides provided by method caller.

  • keys (Array<Symbol>, '*') (defaults to: '*')

    keys for the defaults you want to use.

  • extras (Hash<Symbol, Object>) (defaults to: {})

    extra keys and values to add to the returned defaults.

Returns:

  • (Hash<Symbol, Object>)

    defaults to use in the method call.



69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/cmds/util/defaults.rb', line 69

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).

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

Returns:

  • (String)

    the command's stderr.

See Also:



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

Returns:

  • (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) ⇒ Object

shortcut for Shellwords.escape

also makes it easier to change or customize or whatever



19
20
21
# File 'lib/cmds/util.rb', line 19

def self.esc str
  Shellwords.escape str
end

.format(string, with = :squish) ⇒ Object

Formats a command string.

Parameters:

  • string (String)

    Command string to format.

  • with (nil, :squish, :pretty, #call) (defaults to: :squish)

    How to format the command string.



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/cmds/util.rb', line 55

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.

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &io_block (#call & (#arity ∈ {0, 1}))

    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 IOHandler instance for the execution, which it can use to handle input and outputs.

Returns:

  • (Result)

    result with command string, exist status, stdout and stderr.



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).

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

Returns:

  • (String)

    the command's stdout.

See Also:

  • Cmd.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.

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

  • &input_block (#call)

    optional block that returns a string or IO-like readable object to be used as input for the execution.

Returns:

  • (String)

    the command's stdout.

Raises:

  • (SystemCallError)

    if the command fails (non-zero exit status).

See Also:

  • Cmd.out!


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

create a new Cmds instance with the template and parameters and calls #prepare.

Parameters:

  • template (String)

    ERB template parameters are rendered into to create the command string.

  • *args (Array<Object>)

    positional parameters for rendering into the template.

  • **kwds (Hash{Symbol => Object})

    keyword parameters for rendering into the template.

Returns:

  • (String)

    rendered and formatted command string ready to be executed.



40
41
42
43
44
45
46
47
48
# 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) ⇒ Object



72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/cmds/util.rb', line 72

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

.replace_shortcuts(template) ⇒ Object



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
# File 'lib/cmds/util.rb', line 90

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

.spawn(cmd, **opts, &io_block) ⇒ Fixnum

internal core function to spawn and stream inputs and/or outputs using threads.

originally inspired by

https://nickcharlton.net/posts/ruby-subprocesses-with-stdout-stderr-streams.html

with major modifications from looking at Ruby's open3 module.

Parameters:

  • cmd (String)

    shell-ready command string.

  • input (nil | String | #read)

    string or readable input. here so that Cmds instances can pass their @input instance variable -- &io_block overrides it.

  • env (Hash{Symbol | String => Object})

    blah

  • &io_block (#call & (#arity ∈ {0, 1}))

    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 IOHandler instance for the execution, which it can use to handle input and outputs.

Returns:

  • (Fixnum)

    command exit status.

Raises:

  • (ArgumentError)

    if &io_block has arity greater than 1.



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
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
# File 'lib/cmds/spawn.rb', line 49

def self.spawn cmd, **opts, &io_block
  Cmds.debug "entering Cmds#spawn",
    cmd: cmd,
    opts: opts,
    io_block: io_block
  
  env = opts[:env] || {}
  input = opts[:input]
  chdir = opts[:chdir]

  # 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
      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
      input = handler.in unless handler.in.nil?
    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

  # hash of options that will be passed to `spawn`
  spawn_opts = {}
  
  # add chdir if provided
  spawn_opts[:chdir] = chdir if chdir

  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 }..."
    # see if hanlder.out or hanlder.err is a Proc
    if handler.send(sym).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: handler.send(sym)
      spawn_opts[sym] = handler.send(sym)
      # 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.

Parameters:

  • *values (Array<Object>)

    values to tokenize.

Returns:

  • (String)

    tokenized string ready for the shell.



32
33
34
35
36
37
38
39
40
41
42
43
44
# File 'lib/cmds/util.rb', line 32

def self.tokenize *values, **opts
  values.map {|value|
    case value
    when nil
      # nil is just an empty string, NOT an empty string bash token
      ''
    when Hash
      tokenize_options value, **opts
    else
      esc value.to_s
    end
  }.join ' '
end

.tokenize_option(name, value, **opts) ⇒ Array<String>

turn an option name and value into an array of shell-escaped string token suitable for use in a command.

Parameters:

  • name (String)

    string name (one or more characters).

  • value (*)

    value of the option.

  • **opts (Hash)
  • [Symbol] (Hash)

    a customizable set of options

  • [String] (Hash)

    a customizable set of options

Returns:

  • (Array<String>)

    string tokens.



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
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
# File 'lib/cmds/util/tokenize_option.rb', line 38

def self.tokenize_option name, value, **opts
  pp opts: opts
  
  opts = defaults opts, TOKENIZE_OPT_KEYS
  
  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
  
  prefix, separator = if name.length == 1
    # -b <value> style
    ['-', ' ']
  else
    # --blah=<value> style
    ['--', '=']
  end
    
  case value
  when nil
    []
    
  when Array
    # the PITA one
    case opts[:array_mode]
    when :repeat
      # `-b 1 -b 2 -b 3` / `--blah=1 --blah=2 --blah=3` style
      value.flatten.map {|v|
        prefix + esc(name) + separator + esc(v)
      }
      
    when :join
      # `-b 1,2,3` / `--blah=1,2,3` style
      [ prefix + 
        esc(name) + 
        separator + 
        esc(value.join opts[:array_join_string]) ]
      
    when :json
      [prefix + esc(name) + separator + "'" + JSON.dump(value).gsub(%{'}, %{'"'"'}) + "'"]
      
    else
      # SOL
      raise ArgumentError.new NRSER.squish <<-END
        bad array_mode option: #{ opts[:array_mode] }, 
        should be :repeat, :join or :json
      END
      
    end
    
  when true
    # `-b` or `--blah`
    [prefix + esc(name)]
    
  when false
    case opts[:false_mode]
    when :omit
      # don't emit any token for a false boolean
      []
    when :no
      # `--no-blah` style
      # 
      # but there's not really a great way to handle short names...
      # we use `--no-b`
      # 
      ["--no-#{ esc(name) }"]
      
    else
      raise ArgumentError.new NRSER.squish <<-END
        bad :false_mode option: #{ opts[:false_mode] }, 
        should be :omit or :no
      END
    end
    
  else
    # we let .esc handle it
    [prefix + esc(name) + separator + esc(value)]
    
  end
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.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

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.

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (Cmds::Result)

    result of execution with command string, status, stdout and stderr.



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).

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (String)

    the command's chomped stdout.

See Also:



384
385
386
# File 'lib/cmds.rb', line 384

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).

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (String)

    the command's chomped stdout.

Raises:

  • (SystemCallError)

    if the command fails (non-zero exit status).

See Also:



405
406
407
# File 'lib/cmds.rb', line 405

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



232
233
234
235
236
237
238
239
240
241
242
# File 'lib/cmds.rb', line 232

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.



86
87
88
# File 'lib/cmds/util/defaults.rb', line 86

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).

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (String)

    the command's stderr.

See Also:



423
424
425
# File 'lib/cmds.rb', line 423

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.

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (Boolean)

    true if exit code was not 0.



324
325
326
# File 'lib/cmds.rb', line 324

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.

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (Boolean)

    true if exit code was 0.



310
311
312
# File 'lib/cmds.rb', line 310

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).

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &input_block (#call)

    optional block that returns a string or readable object to override @input.

Returns:

  • (String)

    the command's stdout.

See Also:



349
350
351
# File 'lib/cmds.rb', line 349

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).

Parameters:

Returns:

  • (String)

    the command's stdout.

Raises:

  • (SystemCallError)

    if the command fails (non-zero exit status).

See Also:



367
368
369
# File 'lib/cmds.rb', line 367

def out! *args, **kwds, &input
  capture(*args, **kwds, &input).assert.out
end

#prepare(*args, **kwds) ⇒ String

prepare a shell-safe command string for execution.

Returns:

  • (String)

    the prepared command string.



296
297
298
# File 'lib/cmds.rb', line 296

def prepare *args, **kwds
  @last_prepared_cmd = Cmds.format render(*args, **kwds), self.format
end

#proxyObject



329
330
331
332
333
# File 'lib/cmds.rb', line 329

def proxy
  stream do |io|
    io.in = $stdin
  end
end

#render(*args, **kwds) ⇒ String

Note:

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.

Returns:

  • (String)

    the rendered command string.



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
# File 'lib/cmds.rb', line 258

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.

Parameters:

  • &io_block (nil | String | #read)

    string or readable IO-like object to use as input to the command.

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

Returns:

  • (Fixnum)

    command exit status.



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.

Parameters:

  • *args (Array<Object>)

    positional parameters to append to those in @args for rendering into the command string.

  • **kwds (Hash{Symbol => Object})

    keyword parameters that override those in @kwds for rendering into the command string.

  • &io_block (nil | String | #read)

    string or readable IO-like object to use as input to the command.

Raises:

  • (SystemCallError)

    if exit status 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