cmds 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/cmds.rb CHANGED
@@ -1,393 +1,36 @@
1
1
  # stdlib
2
2
  require 'shellwords'
3
3
  require 'open3'
4
- require 'erubis'
4
+ require 'thread'
5
5
 
6
6
  # deps
7
7
  require 'nrser'
8
8
 
9
9
  # project
10
+ require "cmds/capture"
11
+ require "cmds/debug"
12
+ require "cmds/erb_context"
13
+ require "cmds/io_handler"
14
+ require "cmds/pipe"
15
+ require "cmds/result"
16
+ require "cmds/shell_eruby"
17
+ require "cmds/stream"
18
+ require "cmds/sugar"
19
+ require "cmds/util"
10
20
  require "cmds/version"
11
21
 
12
22
  class Cmds
13
- class Result
14
- attr_reader :cmd, :status, :out, :err
15
-
16
- def initialize cmd, status, out, err
17
- @cmd = cmd
18
- @status = status
19
- @out = out
20
- @err = err
21
- end
22
-
23
- def ok?
24
- @status == 0
25
- end
26
-
27
- def error?
28
- ! ok?
29
- end
30
-
31
- # raises an error if there was one
32
- def raise_error
33
- if error?
34
- msg = NRSER.squish <<-BLOCK
35
- command `#{ @cmd }` exited with status #{ @status }
36
- and stderr output #{ err.inspect }
37
- BLOCK
38
-
39
- raise SystemCallError.new msg, @status
40
- end
41
- end
42
- end
43
-
44
- # extension of Erubis' EscapedEruby (which auto-escapes `<%= %>` and
45
- # leaves `<%== %>` raw) that calls `Cmds.expand_sub` on the value
46
- class ShellEruby < Erubis::EscapedEruby
47
- def escaped_expr code
48
- "::Cmds.expand_sub(#{code.strip})"
49
- end
50
- end
51
-
52
- class ERBContext < BasicObject
53
- def initialize args, kwds
54
- @args = args
55
- @kwds = kwds
56
- @arg_index = 0
57
- end
58
-
59
- def method_missing sym, *args, &block
60
- if args.empty? && block.nil?
61
- if sym.to_s[-1] == '?'
62
- key = sym.to_s[0...-1].to_sym
63
- @kwds[key]
64
- else
65
- @kwds.fetch sym
66
- end
67
- else
68
- super
69
- end
70
- end
71
-
72
- def get_binding
73
- ::Kernel.send :binding
74
- end
75
-
76
- def arg
77
- @args.fetch(@arg_index).tap {@arg_index += 1}
78
- end
79
- end
80
-
81
- # shortcut for Shellwords.escape
82
- #
83
- # also makes it easier to change or customize or whatever
84
- def self.esc str
85
- Shellwords.escape str
86
- end
87
-
88
- # escape option hash.
89
- #
90
- # this is only useful for the two common option styles:
91
- #
92
- # - single character keys become `-<char> <value>`
93
- #
94
- # {x: 1} => "-x 1"
95
- #
96
- # - longer keys become `--<key>=<value>` options
97
- #
98
- # {blah: 2} => "--blah=2"
99
- #
100
- # if you have something else, you're going to have to just put it in
101
- # the cmd itself, like:
102
- #
103
- # Cmds "blah -assholeOptionOn:%{s}", "ok"
104
- #
105
- # or whatever similar shit said command requires.
106
- #
107
- # however, if the value is an Array, it will repeat the option for each
108
- # value:
109
- #
110
- # {x: [1, 2, 3]} => "-x 1 -x 2 -x 3"
111
- # {blah: [1, 2, 3]} => "--blah=1 --blah=2 --blah=3"
112
- #
113
- # i can't think of any right now, but i swear i've seen commands that take
114
- # opts that way.
115
- #
116
- def self.expand_option_hash hash
117
- hash.map {|key, values|
118
- # keys need to be strings
119
- key = key.to_s unless key.is_a? String
120
-
121
- [key, values]
122
-
123
- }.sort {|(key_a, values_a), (key_b, values_b)|
124
- # sort by the (now string) keys
125
- key_a <=> key_b
126
-
127
- }.map {|key, values|
128
- # for simplicity's sake, treat all values like an array
129
- values = [values] unless values.is_a? Array
130
-
131
- # keys of length 1 expand to `-x v` form
132
- expanded = if key.length == 1
133
- values.map {|value|
134
- if value.nil?
135
- "-#{ esc key }"
136
- else
137
- "-#{ esc key } #{ esc value}"
138
- end
139
- }
140
-
141
- # longer keys expand to `--key=value` form
142
- else
143
- values.map {|value|
144
- if value.nil?
145
- "--#{ esc key }"
146
- else
147
- "--#{ esc key }=#{ esc value }"
148
- end
149
- }
150
- end
151
- }.flatten.join ' '
152
- end # ::expand_option_hash
153
-
154
- # expand one of the substitutions
155
- def self.expand_sub sub
156
- case sub
157
- when nil
158
- # nil is just an empty string, NOT an empty string bash token
159
- ''
160
- when Hash
161
- expand_option_hash sub
162
- else
163
- esc sub.to_s
164
- end
165
- end # ::expand_sub
166
-
167
- # substitute values into a command, escaping them for the shell and
168
- # offering convenient expansions for some structures.
169
- #
170
- # `cmd` is a string that can be substituted via ruby's `%` operator, like
171
- #
172
- # "git diff %s"
173
- #
174
- # for positional substitution, or
175
- #
176
- # "git diff %{path}"
177
- #
178
- # for keyword substitution.
179
- #
180
- # `subs` is either:
181
- #
182
- # - an Array when `cmd` has positional placeholders
183
- # - a Hash when `cmd` has keyword placeholders.
184
- #
185
- # the elements of the `subs` array or values of the `subs` hash are:
186
- #
187
- # - strings that are substituted into `cmd` after being escaped:
188
- #
189
- # sub "git diff %{path}", path: "some path/to somewhere"
190
- # # => 'git diff some\ path/to\ somewhere'
191
- #
192
- # - hashes that are expanded into options:
193
- #
194
- # sub "psql %{opts} %{database} < %{filepath}",
195
- # database: "blah",
196
- # filepath: "/where ever/it/is.psql",
197
- # opts: {
198
- # username: "bingo bob",
199
- # host: "localhost",
200
- # port: 12345,
201
- # }
202
- # # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql'
203
- #
204
- def self.sub cmd, args = [], kwds = {}
205
- raise TypeError.new("args must be an Array") unless args.is_a? Array
206
- raise TypeError.new("kwds must be an Hash") unless kwds.is_a? Hash
207
-
208
- context = ERBContext.new(args, kwds)
209
- erb = ShellEruby.new(replace_shortcuts cmd)
210
-
211
- NRSER.squish erb.result(context.get_binding)
212
- end # ::sub
213
-
214
- def self.subs_to_args_kwds_input subs
215
- args = []
216
- kwds = {}
217
- input = nil
218
-
219
- case subs.length
220
- when 0
221
- # nothing to do
222
- when 1
223
- # can either be a hash, which is interpreted as a keywords,
224
- # or an array, which is interpreted as positional arguments
225
- case subs[0]
226
- when Hash
227
- kwds = subs[0]
228
-
229
- when Array
230
- args = subs[0]
231
-
232
- else
233
- raise TypeError.new NRSER.squish <<-BLOCK
234
- first *subs arg must be Array or Hash, not #{ subs[0].inspect }
235
- BLOCK
236
- end
237
-
238
- when 2, 3
239
- # first arg needs to be an array, second a hash, and optional third
240
- # can be input
241
- unless subs[0].is_a? Array
242
- raise TypeError.new NRSER.squish <<-BLOCK
243
- first *subs arg needs to be an array, not #{ subs[0].inspect }
244
- BLOCK
245
- end
246
-
247
- unless subs[1].is_a? Hash
248
- raise TypeError.new NRSER.squish <<-BLOCK
249
- second *subs arg needs to be a Hash, not #{ subs[1].inspect }
250
- BLOCK
251
- end
252
-
253
- args, kwds, input = subs
254
- else
255
- raise ArgumentError.new NRSER.squish <<-BLOCK
256
- must provide one or two *subs arguments, received #{ 1 + subs.length }
257
- BLOCK
258
- end
259
-
260
- [args, kwds, input]
261
- end
262
-
263
- # create a new Cmd from template and subs and call it
264
- def self.run template, *subs
265
- args, kwds, input = subs_to_args_kwds_input subs
266
- self.new(template, args: args, kwds: kwds, input: input).call
267
- end
268
-
269
- def self.ok? template, *subs
270
- args, kwds, input = subs_to_args_kwds_input subs
271
- self.new(template, args: args, kwds: kwds, input: input).ok?
272
- end
273
-
274
- def self.error? template, *subs
275
- args, kwds, input = subs_to_args_kwds_input subs
276
- self.new(template, args: args, kwds: kwds, input: input).error?
277
- end
278
-
279
- def self.raise_on_error template, *subs
280
- args, kwds, input = subs_to_args_kwds_input subs
281
- self.new(
282
- template,
283
- args: args,
284
- kwds: kwds,
285
- input: input,
286
- raise_on_error: true
287
- ).call
288
- end
289
-
290
- def self.replace_shortcuts template
291
- template
292
- .gsub(
293
- # %s => <%= arg %>
294
- /(\A|[[:space:]])\%s(\Z|[[:space:]])/,
295
- '\1<%= arg %>\2'
296
- )
297
- .gsub(
298
- # %%s => %s (escpaing)
299
- /(\A|[[:space:]])(\%+)\%s(\Z|[[:space:]])/,
300
- '\1\2s\3'
301
- )
302
- .gsub(
303
- # %{key} => <%= key %>, %{key?} => <%= key? %>
304
- /(\A|[[:space:]])\%\{([a-zA-Z_]+\??)\}(\Z|[[:space:]])/,
305
- '\1<%= \2 %>\3'
306
- )
307
- .gsub(
308
- # %%{key} => %{key}, %%{key?} => %{key?} (escpaing)
309
- /(\A|[[:space:]])(\%+)\%\{([a-zA-Z_]+\??)\}(\Z|[[:space:]])/,
310
- '\1\2{\3}\4'
311
- )
312
- .gsub(
313
- # %<key>s => <%= key %>, %<key?>s => <%= key? %>
314
- /(\A|[[:space:]])\%\<([a-zA-Z_]+\??)\>s(\Z|[[:space:]])/,
315
- '\1<%= \2 %>\3'
316
- )
317
- .gsub(
318
- # %%<key>s => %<key>s, %%<key?>s => %<key?>s (escaping)
319
- /(\A|[[:space:]])(\%+)\%\<([a-zA-Z_]+\??)\>s(\Z|[[:space:]])/,
320
- '\1\2<\3>s\4'
321
- )
322
- end
323
-
324
- attr_reader :tempalte, :args, :kwds, :input, :raise_on_error
23
+ attr_reader :template, :args, :kwds, :input, :assert
325
24
 
326
25
  def initialize template, opts = {}
26
+ Cmds.debug "Cmds constructed",
27
+ template: template,
28
+ options: opts
29
+
327
30
  @template = template
328
31
  @args = opts[:args] || []
329
32
  @kwds = opts[:kwds] || {}
330
33
  @input = opts[:input] || nil
331
- @raise_on_error = opts[:raise_on_error] || false
332
- end #initialize
333
-
334
- def call *subs
335
- # merge any stored args and kwds and get any overriding input
336
- args, kwds, input = merge_subs subs
337
-
338
- cmd = Cmds.sub @template, args, kwds
339
-
340
- out, err, status = if input.nil?
341
- Open3.capture3 cmd
342
- else
343
- Open3.capture3 cmd, stdin_data: input
344
- end
345
-
346
- result = Cmds::Result.new cmd, status.exitstatus, out, err
347
-
348
- result.raise_error if @raise_on_error
349
-
350
- return result
351
- end #call
352
-
353
- # returns a new `Cmds` with the subs merged in
354
- def curry *subs
355
- args, kwds, input = merge_subs(subs)
356
- self.class.new @template, args: args, kwds: kwds, input: input
357
- end
358
-
359
- def ok?
360
- call.ok?
361
- end
362
-
363
- def error?
364
- call.error?
365
- end
366
-
367
- private
368
-
369
- def merge_subs subs
370
- # break `subs` into `args` and `kwds`
371
- args, kwds, input = Cmds.subs_to_args_kwds_input subs
372
-
373
- # use any default input if we didn't get a new one
374
- input = @input if input.nil?
375
-
376
- [@args + args, @kwds.merge(kwds), input]
377
- end #merge_subs
378
-
379
- # end private
380
- end # Cmds
381
-
382
- # convenience for Cmds::run
383
- def Cmds *args
384
- Cmds.run *args
385
- end
386
-
387
- def Cmds? *args
388
- Cmds.ok? *args
389
- end
390
-
391
- def Cmds! *args
392
- Cmds.raise_on_error *args
393
- end
34
+ @assert = opts[:assert] || false
35
+ end # #initialize
36
+ end
data/scratch/popen3.rb ADDED
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'open3'
4
+ require 'thread'
5
+ require 'pty'
6
+
7
+ master, slave = PTY.open
8
+
9
+ Open3.popen3("./test/tick.rb 10") do |stdin, stdout, stderr, thread|
10
+ {
11
+ stdout => ["out", $stdout],
12
+ stderr => ["err", $stderr],
13
+ }.each do |src, (name, dest)|
14
+
15
+ puts "starting #{ name } thread"
16
+ Thread.new do
17
+ loop do
18
+ puts "getting #{ name } line..."
19
+ line = src.gets
20
+ puts "got #{ name } line."
21
+ if line.nil?
22
+ puts "#{ name } done, breaking."
23
+ break
24
+ else
25
+ puts "wiriting #{ line.bytesize } bytes to #{ name }."
26
+ dest.puts line
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ thread.join
33
+ end
data/scratch/proxy.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'cmds'
2
+
3
+ Cmds.enable_debug do
4
+ Cmds.new("./test/questions.rb").proxy
5
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Cmds::assert" do
4
+ it "should raise an error when the command fails" do
5
+ expect{ Cmds.assert "exit 1" }.to raise_error Errno::EPERM
6
+ end
7
+
8
+ it "should do the same for Cmds!" do
9
+ expect{ Cmds! "exit 1" }.to raise_error Errno::EPERM
10
+ end
11
+
12
+ it "should be chainable when the command is ok" do
13
+ expect( Cmds!("echo hey").out ).to eq "hey\n"
14
+ expect( Cmds.new("echo hey").capture.assert.out ).to eq "hey\n"
15
+ end
16
+ end # Cmds::run
@@ -0,0 +1,108 @@
1
+ require 'spec_helper'
2
+
3
+ describe "Cmds::capture" do
4
+ it "captures stdout" do
5
+ expect(
6
+ Cmds.new(%{ruby -e '$stdout.puts "hey"'}).capture.out
7
+ ).to eq "hey\n"
8
+ end
9
+
10
+ it "captures stderr" do
11
+ expect(
12
+ Cmds.new(%{ruby -e '$stderr.puts "ho"'}).capture.err
13
+ ).to eq "ho\n"
14
+ end
15
+
16
+ context "echo_cmd.rb 'hello world!'" do
17
+
18
+ shared_examples "executes correctly" do
19
+ it_behaves_like "ok"
20
+
21
+ it "should have 'hello world!' as ARGV[0]" do
22
+ expect( JSON.load(result.out)['ARGV'][0] ).to eq "hello world!"
23
+ end
24
+ end # executes correctly
25
+
26
+ context "positional args" do
27
+ let(:result) {
28
+ Cmds "./test/echo_cmd.rb <%= arg %>", ["hello world!"]
29
+ }
30
+
31
+ it_behaves_like "executes correctly"
32
+ end
33
+
34
+ context "keyword args" do
35
+ let(:result) {
36
+ Cmds "./test/echo_cmd.rb <%= s %>", s: "hello world!"
37
+ }
38
+
39
+ it_behaves_like "executes correctly"
40
+ end
41
+
42
+ end # context echo_cmd.rb 'hello world!'
43
+
44
+ # context "feeding kwargs to args cmd" do
45
+ # let(:result) {
46
+ # Cmds "./test/echo_cmd.rb %s", s: "sup y'all"
47
+ # }
48
+
49
+ # it "" do
50
+ # expect( result.cmd ).to eq nil
51
+ # end
52
+ # end
53
+
54
+ it "should error when second (subs) arg is not a hash or array" do
55
+ expect {
56
+ Cmds "./test/echo_cmd.rb <%= arg %>", "hello world!"
57
+ }.to raise_error TypeError
58
+ end
59
+
60
+ it "is reusable" do
61
+ args_cmd = Cmds.new "./test/echo_cmd.rb <%= arg %>"
62
+ kwds_cmd = Cmds.new "./test/echo_cmd.rb <%= s %>"
63
+
64
+ args = ["arg one", "arg two", "arg three"]
65
+
66
+ args.each do |arg|
67
+ results = [
68
+ args_cmd.capture([arg]),
69
+ kwds_cmd.capture(s: arg)
70
+ ]
71
+
72
+ results.each do |result|
73
+ expect( echo_cmd_argv result ).to eq [arg]
74
+ end
75
+ end
76
+ end # is reusable
77
+
78
+ context "input" do
79
+ let(:input) {
80
+ <<-BLOCK
81
+ one
82
+ two
83
+ three
84
+ four!
85
+ BLOCK
86
+ }
87
+
88
+ it "accepts input via options" do
89
+ cmd = Cmds.new(ECHO_CMD, input: input)
90
+ expect( echo_cmd_stdin cmd.capture ).to eq input
91
+ end
92
+
93
+ it "accepts input via block" do
94
+ cmd = Cmds.new ECHO_CMD
95
+ expect( echo_cmd_stdin cmd.capture { input } ).to eq input
96
+ end
97
+
98
+ it "accepts input from a stream" do
99
+ File.open "./test/lines.txt" do |f|
100
+ input = f.read
101
+ f.rewind
102
+
103
+ cmd = Cmds.new ECHO_CMD
104
+ expect( echo_cmd_stdin cmd.capture { f } ).to eq input
105
+ end
106
+ end
107
+ end # context input
108
+ end # Cmds::capture
@@ -2,13 +2,13 @@ require 'spec_helper'
2
2
 
3
3
  describe "Cmds::curry" do
4
4
  it "currys" do
5
- base = Cmds.new "./test/echo_cmd.rb <%= x %> <%= y %>"
5
+ base = Cmds.new "#{ ECHO_CMD } <%= x %> <%= y %>"
6
6
 
7
7
  x1 = base.curry x: 1
8
8
  x2 = base.curry x: 2
9
9
 
10
- expect_argv( x1.call y: 'why' ).to eq ['1', 'why']
11
- expect_argv( x2.call y: 'who' ).to eq ['2', 'who']
12
- expect_argv( base.call x: 3, y: 4 ).to eq ['3', '4']
10
+ expect( echo_cmd_argv x1.call y: 'why' ).to eq ['1', 'why']
11
+ expect( echo_cmd_argv x2.call y: 'who' ).to eq ['2', 'who']
12
+ expect( echo_cmd_argv base.call x: 3, y: 4 ).to eq ['3', '4']
13
13
  end # it currys
14
14
  end # Cmds::run
@@ -1,8 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe "Cmds::error?" do
4
- it "works" do
4
+ it "works through instance method" do
5
+ expect( Cmds.new("true").error? ).to be false
6
+ expect( Cmds.new("false").error? ).to be true
7
+ end
8
+
9
+ it "works through class method" do
5
10
  expect( Cmds.error? "true").to be false
6
11
  expect( Cmds.error? "false").to be true
7
12
  end
8
- end # Cmds::ok?
13
+ end # Cmds::error?
data/spec/cmds/ok_spec.rb CHANGED
@@ -1,8 +1,18 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe "Cmds::ok?" do
4
- it "works" do
4
+ it "works through instance method" do
5
+ expect( Cmds.new("true").ok? ).to be true
6
+ expect( Cmds.new("false").ok? ).to be false
7
+ end
8
+
9
+ it "works through class method" do
5
10
  expect( Cmds.ok? "true").to be true
6
11
  expect( Cmds.ok? "false").to be false
7
12
  end
13
+
14
+ it "workds through global method" do
15
+ expect( Cmds? "true" ).to be true
16
+ expect( Cmds? "false" ).to be false
17
+ end
8
18
  end # Cmds::ok?