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

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



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

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

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

create a new Cmd from template and subs and call it



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

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

Returns:

  • (Boolean)


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

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)


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

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

.options(subs, input_block) ⇒ Object

::sub



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

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 <<-BLOCK
        first *subs arg must be Array or Hash, not #{ subs[0].inspect }
      BLOCK
    end

  when 2
    # first arg needs to be an array, second a hash
    unless subs[0].is_a? Array
      raise TypeError.new NRSER.squish <<-BLOCK
        first *subs arg needs to be an array, not #{ subs[0].inspect }
      BLOCK
    end

    unless subs[1].is_a? Hash
      raise TypeError.new NRSER.squish <<-BLOCK
        second *subs arg needs to be a Hash, not #{ subs[1].inspect }
      BLOCK
    end

    args, kwds = subs
  else
    raise ArgumentError.new NRSER.squish <<-BLOCK
      must provide one or two *subs arguments, received #{ 1 + subs.length }
    BLOCK
  end

  return {
    args: args,
    kwds: kwds,
    input: input,
  }
end

.replace_shortcuts(template) ⇒ Object

::options



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

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



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

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

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



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

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

.sub(cmd, args = [], kwds = {}) ⇒ Object

substitute values into a command, escaping them for the shell and offering convenient expansions for some structures.

‘cmd` is a string that can be substituted via ruby’s ‘%` operator, like

"git diff %s"

for positional substitution, or

"git diff %{path}"

for keyword substitution.

‘subs` is either:

  • an Array when ‘cmd` has positional placeholders

  • a Hash when ‘cmd` has keyword placeholders.

the elements of the ‘subs` array or values of the `subs` hash are:

  • strings that are substituted into ‘cmd` after being escaped:

    sub "git diff %{path}", path: "some path/to somewhere"
    # => 'git diff some\ path/to\ somewhere'
    
  • hashes that are expanded into options:

    sub "psql %{opts} %{database} < %{filepath}",
      database: "blah",
      filepath: "/where ever/it/is.psql",
      opts: {
        username: "bingo bob",
        host: "localhost",
        port: 12345,
      }
    # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql'
    

Raises:

  • (TypeError)


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

def self.sub cmd, 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 cmd)

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

Instance Method Details

#callObject

instance methods



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

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



229
230
231
# File 'lib/cmds/util.rb', line 229

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

#error?Boolean

Returns:

  • (Boolean)


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

def error?
  stream != 0
end

#ok?Boolean

Returns:

  • (Boolean)


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

def ok?
  stream == 0
end

#proxyObject

def assert

capture.raise_error

end



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

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

#stream(*subs, &input_block) ⇒ Object

stream inputs and/or outputs

originally inspired by

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