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.6"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template, opts = {}) ⇒ 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



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

.error?(template, *subs, &input_block) ⇒ 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



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"

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

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



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

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

#error?Boolean



63
64
65
# File 'lib/cmds/sugar.rb', line 63

def error?
  stream != 0
end

#ok?Boolean



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

def ok?
  stream == 0
end

#proxyObject

def assert capture.raise_error end



71
72
73
74
75
# File 'lib/cmds/sugar.rb', line 71

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