cmds 0.0.3 → 0.0.4

Sign up to get free protection for your applications and to get access to all the features.
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?