Class: Cmds

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

Overview

debug logging stuff

Defined Under Namespace

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

Constant Summary collapse

VERSION =
"0.0.8"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template, opts = {}) ⇒ Cmds

Returns a new instance of Cmds.



25
26
27
28
29
30
31
32
33
34
35
# File 'lib/cmds.rb', line 25

def initialize template, opts = {}
  Cmds.debug "Cmds constructed",
    template: template,
    options: opts

  @template = template
  @args = opts[:args] || []
  @kwds = opts[:kwds] || {}
  @input = opts[:input] || nil
  @assert = opts[:assert] || false
end

Instance Attribute Details

#argsObject (readonly)

Returns the value of attribute args.



23
24
25
# File 'lib/cmds.rb', line 23

def args
  @args
end

#assertObject (readonly)

Returns the value of attribute assert.



23
24
25
# File 'lib/cmds.rb', line 23

def assert
  @assert
end

#inputObject (readonly)

Returns the value of attribute input.



23
24
25
# File 'lib/cmds.rb', line 23

def input
  @input
end

#kwdsObject (readonly)

Returns the value of attribute kwds.



23
24
25
# File 'lib/cmds.rb', line 23

def kwds
  @kwds
end

#templateObject (readonly)

Returns the value of attribute template.



23
24
25
# File 'lib/cmds.rb', line 23

def template
  @template
end

Class Method Details

.assert(template, *subs, &input_block) ⇒ Object



39
40
41
42
43
44
# File 'lib/cmds/sugar.rb', line 39

def self.assert template, *subs, &input_block
  new(
    template,
    options(subs, input_block).merge!(assert: true)
  ).capture
end

.capture(template, *subs, &input_block) ⇒ Result

create a new Cmd from template and subs and call it

Returns:



27
28
29
# File 'lib/cmds/sugar.rb', line 27

def self.capture template, *subs, &input_block
  new(template, options(subs, input_block)).capture
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

.err(template, *subs, &input_block) ⇒ Object

captures and returns stderr (sugar for Cmds.capture(*args).err).

Parameters:

See Also:



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

def self.err template, *subs, &input_block
  capture(template, *subs, &input_block).err
end

.error?(template, *subs, &input_block) ⇒ Boolean

Returns:

  • (Boolean)


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

def self.error? template, *subs, &input_block
  new(template, options(subs, input_block)).error?
end

.esc(str) ⇒ Object

shortcut for Shellwords.escape

also makes it easier to change or customize or whatever



9
10
11
# File 'lib/cmds/util.rb', line 9

def self.esc str
  Shellwords.escape str
end

.expand_option_hash(hash) ⇒ 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.



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

def self.expand_option_hash hash
  hash.map {|key, values|
    # keys need to be strings
    key = key.to_s unless key.is_a? String

    [key, values]

  }.sort {|(key_a, values_a), (key_b, values_b)|
    # sort by the (now string) keys
    key_a <=> key_b

  }.map {|key, values|
    # for simplicity's sake, treat all values like an array
    values = [values] unless values.is_a? Array

    # keys of length 1 expand to `-x v` form
    expanded = if key.length == 1
      values.map {|value|
        if value.nil?
          "-#{ esc key }"
        else
          "-#{ esc key } #{ esc value}"
        end
      }

    # longer keys expand to `--key=value` form
    else
      values.map {|value|
        if value.nil?
          "--#{ esc key }"
        else
          "--#{ esc key }=#{ esc value }"
        end
      }
    end
  }.flatten.join ' '
end

.expand_sub(sub) ⇒ Object

expand one of the substitutions



80
81
82
83
84
85
86
87
88
89
90
# File 'lib/cmds/util.rb', line 80

def self.expand_sub sub
  case sub
  when nil
    # nil is just an empty string, NOT an empty string bash token
    ''
  when Hash
    expand_option_hash sub
  else
    esc sub.to_s
  end
end

.ok?(template, *subs, &input_block) ⇒ Boolean

Returns:

  • (Boolean)


31
32
33
# File 'lib/cmds/sugar.rb', line 31

def self.ok? template, *subs, &input_block
  new(template, options(subs, input_block)).ok?
end

.options(subs, input_block) ⇒ Object

::sub



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

def self.options subs, input_block
  args = []
  kwds = {}
  input = input_block.nil? ? nil : input_block.call

  case subs.length
  when 0
    # nothing to do
  when 1
    # can either be a hash, which is interpreted as a keywords,
    # or an array, which is interpreted as positional arguments
    case subs[0]
    when Hash
      kwds = subs[0]

    when Array
      args = subs[0]

    else
      raise TypeError.new NRSER.squish "        first *subs arg must be Array or Hash, not \#{ subs[0].inspect }\n      BLOCK\n    end\n\n  when 2\n    # first arg needs to be an array, second a hash\n    unless subs[0].is_a? Array\n      raise TypeError.new NRSER.squish <<-BLOCK\n        first *subs arg needs to be an array, not \#{ subs[0].inspect }\n      BLOCK\n    end\n\n    unless subs[1].is_a? Hash\n      raise TypeError.new NRSER.squish <<-BLOCK\n        second *subs arg needs to be a Hash, not \#{ subs[1].inspect }\n      BLOCK\n    end\n\n    args, kwds = subs\n  else\n    raise ArgumentError.new NRSER.squish <<-BLOCK\n      must provide one or two *subs arguments, received \#{ 1 + subs.length }\n    BLOCK\n  end\n\n  return {\n    args: args,\n    kwds: kwds,\n    input: input,\n  }\nend\n"

.out(template, *subs, &input_block) ⇒ Object

captures and returns stdout (sugar for Cmds.capture(*args).out).

Parameters:

See Also:



66
67
68
# File 'lib/cmds/sugar.rb', line 66

def self.out template, *subs, &input_block
  capture(template, *subs, &input_block).out
end

.out!(template, *subs, &input_block) ⇒ Object

captures and returns stdout, raising an error if the command fails.

Parameters:

See Also:



82
83
84
85
86
87
# File 'lib/cmds/sugar.rb', line 82

def self.out! template, *subs, &input_block
  Cmds.new(
    template,
    options(subs, input_block).merge!(assert: true),
  ).capture.out
end

.replace_shortcuts(template) ⇒ Object

::options



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

def self.replace_shortcuts template
  template
    .gsub(
      # %s => <%= arg %>
      /(?<=\A|[[:space:]])\%s(?=\Z|[[:space:]])/,
      '<%= arg %>'
    )
    .gsub(
      # %%s => %s (escpaing)
      /(?<=\A|[[:space:]])(\%+)\%s(?=\Z|[[:space:]])/,
      '\1s'
    )
    .gsub(
      # %{key} => <%= key %>, %{key?} => <%= key? %>
      /(?<=\A|[[:space:]])\%\{([a-zA-Z_]+\??)\}(?=\Z|[[:space:]])/,
      '<%= \1 %>'
    )
    .gsub(
      # %%{key} => %{key}, %%{key?} => %{key?} (escpaing)
      /(?<=\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

.stream(template, *subs, &input_block) ⇒ Object



46
47
48
# File 'lib/cmds/sugar.rb', line 46

def self.stream template, *subs, &input_block
  Cmds.new(template).stream *subs, &input_block
end

.stream!(template, *subs, &input_block) ⇒ Object



50
51
52
# File 'lib/cmds/sugar.rb', line 50

def self.stream! template, *subs, &input_block
  Cmds.new(template, assert: true).stream *subs, &input_block
end

.sub(template, args = [], kwds = {}) ⇒ String

substitute values into a command template, escaping them for the shell and offering convenient expansions for some structures. uses ERB templating, so logic is supported as well.

check out the README for details on use.

Parameters:

  • template (String)

    command template with token to be replaced / expanded / escaped.

  • args (Array) (defaults to: [])

    positional substitutions for occurances of

    • <%= arg %>
    • %s

    tokens in the template parameter.

  • kwds (Hash) (defaults to: {})

    keyword subsitutions for occurances of the form

    • <%= key %>
    • %{key}
    • %<key>s

    as well as optional

    • <%= key? %>
    • %{key?}
    • %<key?>s

    tokens in the template parameter (where key is replaced with the symbol name in the hash).

Returns:

  • (String)

    formated command string suitable for execution.

Raises:

  • (TypeError)

    if args is not an Array.

  • (TypeError)

    if kwds is not a Hash.



127
128
129
130
131
132
133
134
135
# File 'lib/cmds/util.rb', line 127

def self.sub template, args = [], kwds = {}
  raise TypeError.new("args must be an Array") unless args.is_a? Array
  raise TypeError.new("kwds must be an Hash") unless kwds.is_a? Hash

  context = ERBContext.new(args, kwds)
  erb = ShellEruby.new replace_shortcuts(template)

  NRSER.squish erb.result(context.get_binding)
end

Instance Method Details

#callObject

instance methods



108
# File 'lib/cmds/sugar.rb', line 108

alias_method :call, :capture

#capture(*subs, &input_block) ⇒ Object

invokes the command and returns a Result with the captured outputs



3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
# File 'lib/cmds/capture.rb', line 3

def capture *subs, &input_block
  Cmds.debug "entering Cmds#capture",
    subs: subs,
    input_block: input_block

  # merge any stored args and kwds and replace input if provided
  options = merge_options subs, input_block
  Cmds.debug "merged options:",
    options: options

  # build the command string
  cmd = Cmds.sub @template, options[:args], options[:kwds]
  Cmds.debug "built command string: #{ cmd.inspect }"

  out = ''
  err = ''

  Cmds.debug "calling Cmds#really_stream..."
  status = really_stream cmd, options do |io|
    # send the input to stream, which sends it to spawn
    io.in = options[: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#really_stream completed",
    status: status

  # build a Result
  # result = Cmds::Result.new cmd, status, out_reader.value, err_reader.value
  result = Cmds::Result.new 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

#curry(*subs, &input_block) ⇒ Object

returns a new Cmds with the subs and input block merged in



227
228
229
# File 'lib/cmds/util.rb', line 227

def curry *subs, &input_block
  self.class.new @template, merge_options(subs, input_block)
end

#err(*subs, &input_block) ⇒ String

captures and returns stdout (sugar for #capture(*subs, &input_block).err).

Parameters:

Returns:

  • (String)

    the command's stdout.

See Also:



181
182
183
# File 'lib/cmds/sugar.rb', line 181

def err *subs, &input_block
  capture(*subs, &input_block).err
end

#error?Boolean

Returns:

  • (Boolean)


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

def error?
  stream != 0
end

#ok?Boolean

Returns:

  • (Boolean)


110
111
112
# File 'lib/cmds/sugar.rb', line 110

def ok?
  stream == 0
end

#out(*subs, &input_block) ⇒ String

captures and returns stdout (sugar for #capture(*subs, &input_block).out).

Parameters:

Returns:

  • (String)

    the command's stdout.

See Also:



142
143
144
# File 'lib/cmds/sugar.rb', line 142

def out *subs, &input_block
  capture(*subs, &input_block).out
end

#out!(*subs, &input_block) ⇒ String

captures and returns stdout (sugar for #capture(*subs, &input_block).out).

Parameters:

Returns:

  • (String)

    the command's stdout.

See Also:



160
161
162
163
164
165
# File 'lib/cmds/sugar.rb', line 160

def out! *subs, &input_block
  self.class.new(
    @template,
    merge_options(subs, input_block).merge!(assert: true),
  ).capture.out
end

#proxyObject

def assert capture.raise_error end



122
123
124
125
126
# File 'lib/cmds/sugar.rb', line 122

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

#stream(*subs, &input_block) ⇒ Object

stream inputs and/or outputs

originally inspired by

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

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



10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/cmds/stream.rb', line 10

def stream *subs, &input_block
  Cmds.debug "entering Cmds#stream",
    subs: subs,
    input_block: input_block

  # use `merge_options` to get the args and kwds (we will take custom
  # care of input in _stream)
  options = merge_options subs, nil

  # build the command string
  cmd = Cmds.sub @template, options[:args], options[:kwds]

  # call the internal function
  really_stream cmd, options, &input_block
end