hysh 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3f13194edb86a917872e0c88c067ce33b7e18dcf
4
+ data.tar.gz: 4e743290d7746ad6fabc62deeb058f8ecfe9effd
5
+ SHA512:
6
+ metadata.gz: 7380c1dd990f66b059b4969f0d5a9e29165a5a207a9281c584d97497b74613f704c736a4cc1117876a1ef8ff3f6a0248500fdd46ae17d099a6d2b859852cc02c
7
+ data.tar.gz: 6186d9e71eb8148c0dc3d99755e2f89fb666818ef14c6e483cba93f73ce6f2808224ea5a3181c2769418acbcbac04459766b61b4804f825b881ce060d0d889bb
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gem 'rake'
4
+ gem 'rspec'
@@ -0,0 +1,116 @@
1
+ = Ruby HYSH
2
+
3
+ Ruby HYSH stands for Huang Ying's SHell in Ruby.
4
+
5
+ Bash interactive shell and scripts are very important tools to use
6
+ Linux/Unix. But I don't like the syntax of bash, would rather to do
7
+ that in Ruby. This work is based on HYSH (Huang Ying's SHell in
8
+ Common Lisp: https://github.com/hying-caritas/hysh).
9
+
10
+ == Example
11
+
12
+ def dpkg_installed(package_names = nil)
13
+ Hysh.out_lines ->{
14
+ Hysh.pipe ['dpkg', '-l'],
15
+ if package_names
16
+ ['egrep', "(#{package_names.join '|'})"]
17
+ else
18
+ ['cat']
19
+ end
20
+ }
21
+ end
22
+
23
+ or write the filter in Ruby.
24
+
25
+ def dpkg_installed(package_names = nil)
26
+ Hysh.out_lines ->{
27
+ Hysh.pipe ['dpkg', '-l'] {
28
+ proc_line = if package_names
29
+ ->l{
30
+ if package_names.any? { |pkg|
31
+ l.index pkg
32
+ }
33
+ l
34
+ end
35
+ }
36
+ else
37
+ ->l{ l }
38
+ end
39
+ Hysh.filter_line &proc_line
40
+ }
41
+ }
42
+ end
43
+
44
+ Compared with Kernel.system, HYSH provides different coding style for
45
+ IO redirection, Environment manipulating, Setting current directory,
46
+ pipe line support without shell, and writing pipeline filter in Ruby.
47
+
48
+ == Common conventions
49
+
50
+ There are mainly two categories of functions in HYSH. Some functions
51
+ compute (run a function or command), some other functions setup
52
+ environment (IO redirection, manipulating environment, changing
53
+ current directory, etc.) for computing.
54
+
55
+ Functions to setup environment for some computing have one parameter
56
+ to several parameters (sometimes via block too) to specify the one to
57
+ several computing (for example, pipe). Most computing functions
58
+ return whether computing is successful ($? holds the details status if
59
+ the computing is synchronous and run as the process). Most functions
60
+ to setup environment will return the return value of one of the given
61
+ computing. Most out_, and io_ family functions will return two values,
62
+ the first is the string returned, the second is the run value of the
63
+ computing.
64
+
65
+ == Run external program
66
+
67
+ Unlike Kernel.system, HYSH typically uses only a very basic run
68
+ function (although it is possible to specify options), because most IO
69
+ redirection and glue between programs are done in Ruby functions.
70
+
71
+ == IO redirection
72
+
73
+ IO redirection in Unix process world means replace the original
74
+ standard input, output, etc. file descriptors with file, pipe, etc.
75
+ In HYSH, IO redirection is defined for Ruby too. That means replace
76
+ the original $stdin, $stdout, and $stderr, etc. IOs with other IOs of
77
+ file, pipe, etc. So for an IO redirection, a Ruby global IO variable
78
+ name and a file descriptor number can be specified. After that is
79
+ done, all Ruby function will reference the replaced IOs for the global
80
+ IO variables, and the specified file descriptors redirection will be
81
+ setup for the external programs too.
82
+
83
+ == Process
84
+
85
+ The process in HYSH is used to represent a Ruby fork or Unix process.
86
+ The child processes will inherited all IO redirection, environment
87
+ variables, and current directory, etc. from their parent processes.
88
+
89
+ == Glue between processes
90
+
91
+ The most important glue is pipeline. I think this is the flagship of
92
+ UNIX worlds. Now we can do that in Ruby. Any processes can be
93
+ connected with pipeline, regardless Ruby fork or Unix process.
94
+
95
+ Other glue mechanisms are provided too, including and, or, and
96
+ sequence etc.
97
+
98
+ == External program error processing
99
+
100
+ External program error is defined as exiting with non-zero status,
101
+ that is, failed in UNIX sense. It can be ignored, warned or an
102
+ exception can be raised to stop running the following code.
103
+
104
+ == Arbitrary combination
105
+
106
+ The power of HYSH is that it provide a more flexible way to combine
107
+ external programs, IO redirection, glue (pipeline, etc.), etc. with
108
+ Ruby.
109
+
110
+ For example, to encapsulate some external filter program in Ruby with
111
+ string as input and output. It can be accomplished with:
112
+
113
+ (Hysh.in_s input-string (Hysh.out_s (Hysh.run filter arg1 arg2 ...)))
114
+
115
+ For given input-string and arguments, the form will return the result
116
+ string and exit success status of the filter.
@@ -0,0 +1,17 @@
1
+ # -*- ruby-indent-level: 2; -*-
2
+ require "rspec/core/rake_task"
3
+ require "rdoc/task"
4
+
5
+ task :default => 'spec'
6
+
7
+ desc "Run all specs"
8
+ RSpec::Core::RakeTask.new(:spec) { |t|
9
+ t.rspec_opts = "--colour"
10
+ t.pattern = "spec/*_spec.rb"
11
+ }
12
+
13
+ desc "Generate document"
14
+ RDoc::Task.new { |rdoc|
15
+ rdoc.main = "README.rdoc"
16
+ rdoc.rdoc_files.include "README.rdoc", "lib/*.rb"
17
+ }
@@ -0,0 +1,836 @@
1
+ # -*- ruby-indent-level: 2; -*-
2
+ #
3
+ # Hysh: Huang Ying's Shell in Ruby
4
+ #
5
+ # Copyright (c) 2015 Huang Ying <huang.ying.caritas@gmail.com>
6
+ #
7
+ # LGPL v2.0 or later.
8
+
9
+ require 'tempfile'
10
+
11
+ # Hysh stands for Huang Ying's Shell in Ruby. Like other shells, it
12
+ # can redirect IO, run external programs, and glue processes (like
13
+ # pipeline), etc. One of the best stuff it can do is to write
14
+ # pipeline filter in Ruby.
15
+ #
16
+ module Hysh
17
+ # :stopdoc:
18
+ TEMP_BASE = "hysh-"
19
+ @@redirections = []
20
+ IGNORE = :ignore
21
+ WARN = :warn
22
+ RAISE = :raise
23
+ @@on_command_error = IGNORE
24
+ # :startdoc:
25
+
26
+ # :section: Common Utilities
27
+
28
+ # :call-seq:
29
+ # with_set_globals(var_name, val, ...) { ... }
30
+ #
31
+ # Set the global variable named +var_name+ (a string or symbol) to
32
+ # +val+, then run the block, return the return value of the block.
33
+ # Restore the original value of the variable upon returning.
34
+ # Multiple pairs of +var_name+ and +val+ can be specified.
35
+ def self.with_set_globals(*var_vals)
36
+ orig_var_vals = var_vals.each_slice(2).map { |var, val|
37
+ svar = var.to_s
38
+ unless svar.start_with? '$'
39
+ raise ArgumentError, "Invalid global variable name: #{svar}"
40
+ end
41
+ orig_val = eval(svar)
42
+ [svar, val, orig_val]
43
+ }
44
+ orig_var_vals.each { |var, val|
45
+ eval("#{var} = val")
46
+ }
47
+ yield
48
+ ensure
49
+ if orig_var_vals
50
+ orig_var_vals.each { |var, val, orig_val|
51
+ eval("#{var} = orig_val")
52
+ }
53
+ end
54
+ end
55
+
56
+ # :section: IO Redirection
57
+
58
+ # :call-seq:
59
+ # with_redirect_to(fd, var_name, io) { ... }
60
+ #
61
+ # Set the variable named +var_name+ (usually +$stdin+, +$stdout+,
62
+ # +$stderr, etc.) to +io+ (redirection in Ruby), and arrange to
63
+ # redirect the +fd+ (usally 0, 1, 2, etc. to +io+ for the external
64
+ # programs, then run the block, return the return value of the
65
+ # block. Restore the original value of the variable and cancel the
66
+ # arrangement to external program redirections upon returning.
67
+ def self.with_redirect_to(fd, var, io, &b)
68
+ @@redirections.push([fd, io]) if fd
69
+ if var
70
+ with_set_globals(var, io, &b)
71
+ else
72
+ yield
73
+ end
74
+ ensure
75
+ @@redirections.pop if fd
76
+ end
77
+
78
+ # :call-seq:
79
+ # with_redirect_stdin_to(io) { ... }
80
+ #
81
+ # Set the +$stdin+ to +io+ (redirection in Ruby), and arrange to
82
+ # redirect the file descriptor 0 to +io+ for the external programs,
83
+ # then run the block, return the return value of the block. Restore
84
+ # the original value of the $stdin and cancel the arrangement to
85
+ # external program redirections upon returning.
86
+ def self.with_redirect_stdin_to(io, &b)
87
+ with_redirect_to(0, :$stdin, io, &b)
88
+ end
89
+
90
+ # :call-seq:
91
+ # with_redirect_stdout_to(io) { ... }
92
+ #
93
+ # Set the +$stdout+ to +io+ (redirection in Ruby), and arrange to
94
+ # redirect the file descriptor 1 to +io+ for the external programs,
95
+ # then run the block, return the return value of the block. Restore
96
+ # the original value of the $stdout and cancel the arrangement to
97
+ # external program redirections upon returning.
98
+ def self.with_redirect_stdout_to(io, &b)
99
+ with_redirect_to(1, :$stdout, io, &b)
100
+ end
101
+
102
+ # :call-seq:
103
+ # with_redirect_stderr_to(io) { ... }
104
+ #
105
+ # Set the +$stderr+ to +io+ (redirection in Ruby), and arrange to
106
+ # redirect the file descriptor 2 to +io+ for the external programs,
107
+ # then run the block, return the return value of the block. Restore
108
+ # the original value of the $stderr and cancel the arrangement to
109
+ # external program redirections upon returning.
110
+ def self.with_redirect_stderr_to(io, &b)
111
+ with_redirect_to(2, :$stderr, io, &b)
112
+ end
113
+
114
+ # :call-seq:
115
+ # with_redirect_stdin_file(args...) { ... }
116
+ #
117
+ # Open the file with parameters: +args+, which are same as the
118
+ # parameters of +File.open+. Set the +$stdin+ to the return +io+
119
+ # (redirection in Ruby), and arrange to redirect the file descriptor
120
+ # 0 to the returned +io+ for the external programs, then run the
121
+ # block, return the return value of the block. Restore the original
122
+ # value of the $stdin and cancel the arrangement to external program
123
+ # redirections upon returning.
124
+ def self.with_redirect_stdin_to_file(*args, &b)
125
+ File.open(*args) { |f|
126
+ with_redirect_stdin_to f, &b
127
+ }
128
+ end
129
+
130
+ # :call-seq:
131
+ # with_redirect_stdout_file(args...) { ... }
132
+ #
133
+ # Open the file with parameters: +args+, which are same as the
134
+ # parameters of +File.open+. Set the +$stdout+ to the return +io+
135
+ # (redirection in Ruby), and arrange to redirect the file descriptor
136
+ # 1 to the returned +io+ for the external programs, then run the
137
+ # block, return the return value of the block. Restore the original
138
+ # value of the $stdout and cancel the arrangement to external
139
+ # program redirections upon returning.
140
+ def self.with_redirect_stdout_to_file(*args, &b)
141
+ if args.size == 1
142
+ args.push "w"
143
+ end
144
+ File.open(*args) { |f|
145
+ with_redirect_stdout_to f, &b
146
+ }
147
+ end
148
+
149
+ # :call-seq:
150
+ # with_redirect_stderr_file(args...) { ... }
151
+ #
152
+ # Open the file with parameters: +args+, which are same as the
153
+ # parameters of +File.open+. Set the +$stderr+ to the return +io+
154
+ # (redirection in Ruby), and arrange to redirect the file descriptor
155
+ # 2 to the returned +io+ for the external programs, then run the
156
+ # block, return the return value of the block. Restore the original
157
+ # value of the $stderr and cancel the arrangement to external
158
+ # program redirections upon returning.
159
+ def self.with_redirect_stderr_to_file(*args, &b)
160
+ if args.size == 1
161
+ args.push "w"
162
+ end
163
+ File.open(*args) { |f|
164
+ with_redirect_stderr_to f, &b
165
+ }
166
+ end
167
+
168
+ def self.__out_io(args, options, proc_arg) # :nodoc:
169
+ Tempfile.open(TEMP_BASE) { |tempf|
170
+ tempf.unlink
171
+ ret = nil
172
+ with_redirect_stdout_to(tempf) {
173
+ ret = __run args, options, proc_arg
174
+ }
175
+ tempf.rewind
176
+ stuff = yield tempf
177
+ [stuff, ret]
178
+ }
179
+ end
180
+
181
+ # :call-seq:
182
+ # out_s() { ... } -> [string, any]
183
+ # out_s(function) -> [string, any]
184
+ # out_s(command...[, options]) -> [string, true or false]
185
+ #
186
+ # Collect the output of running the block, or the function specified
187
+ # via +function+ or the external program specified via +command+ and
188
+ # +options+ via stdout redirection. +command+ and +options+
189
+ # parameters are same as that of +Process.spawn+. Return the
190
+ # collected output string and the return value of the block or the
191
+ # function or exit success status of the external program as a two
192
+ # element array. Restore stdout redirection upon returning.
193
+ def self.out_s(*args, &blk)
194
+ __out_io(*__parse_args(args, blk)) { |tempf|
195
+ tempf.read
196
+ }
197
+ end
198
+
199
+ # :call-seq:
200
+ # out_ss() { ... } -> [string, any]
201
+ # out_ss(function) -> [string, any]
202
+ # out_ss(command...[, options]) -> [string, true or false]
203
+ #
204
+ # Same as out_s, except the collected output string are right
205
+ # stripped before return.
206
+ def self.out_ss(*args_in, &blk)
207
+ s, ret = out_s(*args_in, &blk)
208
+ [s.rstrip, ret]
209
+ end
210
+
211
+ # :call-seq:
212
+ # out_lines(funciton) -> [array of string, any]
213
+ # out_lines(command...[, options]) -> [array of string, any]
214
+ # out_lines(function) { |line| ... } -> true or false
215
+ # out_lines(command...[, options]) { |line| ... } -> true or false
216
+ #
217
+ # If no block is supplied, collect the output of running the
218
+ # function specified via +function+ or the external program specified
219
+ # via +command+ and +options+ via stdout redirection. +command+ and
220
+ # +options+ are same as that of +Process.spawn+. Return the
221
+ # collected string as lines and the return value of the block or the
222
+ # function or exit success status of the external program. Restore
223
+ # stdout redirection upon returning.
224
+ #
225
+ # If block is supplied, collect the output of running the function
226
+ # specified via +function+ (in a forked sub-process) or the external
227
+ # program specified via +command+ and +options+ via stdout
228
+ # redirection. +command+ and +options+ are same as that of
229
+ # +Process.spawn+. Feed each line of output to the block as +line+.
230
+ # Return the exit success status of the forked sub-process or the
231
+ # external program. Restore stdout redirection upon returning.
232
+ def self.out_lines(*args_in, &blk)
233
+ args, options, proc_arg = __parse_args args_in
234
+ if block_given?
235
+ __popen(nil, true, nil, args, options, proc_arg) { |pid, stdin, stdout, stderr|
236
+ stdout.each_line(&blk)
237
+ Process.waitpid pid
238
+ __check_command_status args_in
239
+ }
240
+ else
241
+ __out_io(args, options, proc_arg) { |tempf|
242
+ tempf.readlines
243
+ }
244
+ end
245
+ end
246
+
247
+ # :call-seq:
248
+ # out_err_s() { ... } -> [string, any]
249
+ # out_err_s(function) -> [string, any]
250
+ # out_err_s(command...[, options]) -> [string, true or false]
251
+ #
252
+ # Same as out_s, except collect output of stderr too.
253
+ def self.out_err_s(*args_in, &blk)
254
+ args, options, proc_arg = __parse_args args_in, blk
255
+ Tempfile.open(TEMP_BASE) { |tempf|
256
+ tempf.unlink
257
+ ret = nil
258
+ with_redirect_stdout_to(tempf) {
259
+ with_redirect_stderr_to(tempf) {
260
+ ret = __run args, options, proc_arg
261
+ }
262
+ }
263
+ tempf.rewind
264
+ s = tempf.read
265
+ [s, ret]
266
+ }
267
+ end
268
+
269
+ # :call-seq:
270
+ # out_err_ss() { ... } -> [string, any]
271
+ # out_err_ss(function) -> [string, any]
272
+ # out_err_ss(command...[, options]) -> [string, true or false]
273
+ #
274
+ # Same as out_err_s, except the collected output string are right
275
+ # stripped before return.
276
+ def self.out_err_ss(*args_in, &blk)
277
+ s, ret = out_err_s(*args_in, &blk)
278
+ [s.rstrip. ret]
279
+ end
280
+
281
+ def self.__in_io(args, options, proc_arg) # :nodoc:
282
+ Tempfile.open(TEMP_BASE) { |tempf|
283
+ tempf.unlink
284
+ yield tempf
285
+ tempf.rewind
286
+ with_redirect_stdin_to(tempf) {
287
+ __run args, options, proc_arg
288
+ }
289
+ }
290
+ end
291
+
292
+ # :call-seq:
293
+ # in_s(string) { ... } -> any
294
+ # in_s(string, function) -> any
295
+ # in_s(string, command...[, options]) -> true or false
296
+ #
297
+ # Feed the string specified via +string+ to the running of the
298
+ # block, or the function specified via +function+ or the external
299
+ # program specified via +command+ and +options+ via stdin
300
+ # redirection. +command+ and +options+ are same as that of
301
+ # +Process.spawn+. Return the return value of the block or the
302
+ # function or the exit success status of the external program.
303
+ # Restore stdin redirection upon returning.
304
+ def self.in_s(s, *args_in, &blk)
305
+ args, options, proc_arg = __parse_args args_in, blk
306
+ __in_io(args, options, proc_arg) { |tempf|
307
+ tempf.write s
308
+ }
309
+ end
310
+
311
+ # :call-seq:
312
+ # in_lines(lines) { ... } -> any
313
+ # in_lines(lines, function) -> any
314
+ # in_lines(lines, command...[, options]) -> true or false
315
+ #
316
+ # Same as +in_s+, except input string are specified via +lines+
317
+ # (Array of String).
318
+ def self.in_lines(lines, *args_in, &blk)
319
+ args, options, proc_arg = __parse_args args_in, blk
320
+ __in_io(args, options, proc_arg) { |tempf|
321
+ lines.each { |line| tempf.write line }
322
+ }
323
+ end
324
+
325
+ # :call-seq:
326
+ # io_s(string) { ... } -> [string, any]
327
+ # io_s(string, function) -> [string, any]
328
+ # io_s(stirng, command...[, options]) -> [string, true or false]
329
+ #
330
+ # Redirect the stdin and stdout like that of +in_s+ and +out_s+,
331
+ # return value is same of +out_s+.
332
+ def self.io_s(s, *args_in, &blk)
333
+ in_s(s) {
334
+ out_s {
335
+ run *args_in, &blk
336
+ }
337
+ }
338
+ end
339
+
340
+ # :call-seq:
341
+ # io_ss(string) { ... } -> [string, any]
342
+ # io_ss(string, function) -> [string, any]
343
+ # io_ss(stirng, command...[, options]) -> [string, true or false]
344
+ #
345
+ # Same as +io_s+, except the output string is right stripped before
346
+ # returning.
347
+ def self.io_ss(s, *args_in, &blk)
348
+ s = io_s(s, *args_in, &blk)
349
+ s.rstrip
350
+ end
351
+
352
+ # :section: Run Process
353
+
354
+ # :call-seq:
355
+ # ignore_on_command_error() { ... }
356
+ #
357
+ # When running the block, the non-zero exit status of running
358
+ # external program are ignored. The original behavior is restored
359
+ # upon returning.
360
+ def self.ignore_on_command_error(&b)
361
+ with_set_globals(:@@on_command_error, IGNORE, &b)
362
+ end
363
+
364
+ # :call-seq:
365
+ # warn_on_command_error() { ... }
366
+ #
367
+ # When running the block, the warning message will be print to
368
+ # $stderr when the external program exited with non-zero status.
369
+ # The original behavior is restored upon returning.
370
+ def self.warn_on_command_error(&b)
371
+ with_set_globals(:@@on_command_error, WARN, &b)
372
+ end
373
+
374
+ # :call-seq:
375
+ # raise_on_command_error() { ... }
376
+ #
377
+ # When running the block, an +Hysh::CommandError+ exception will be
378
+ # raised when the external program exited with non-zero status. The
379
+ # original behavior is restored upon returning.
380
+ def self.raise_on_command_error(&b)
381
+ with_set_globals(:@@on_command_error, RAISE, &b)
382
+ end
383
+
384
+ def self.__parse_args(args, blk = nil) # :nodoc:
385
+ args = [args] unless args.is_a? Array
386
+ if args.last.is_a?(Hash)
387
+ options = args.pop
388
+ else
389
+ options = {}
390
+ end
391
+ if args.empty?
392
+ if blk.equal? nil
393
+ raise ArgumentError.new('No argument or block!')
394
+ else
395
+ args = [blk]
396
+ proc_arg = true
397
+ end
398
+ else
399
+ proc_arg = args.size == 1 && args.first.is_a?(Proc)
400
+ end
401
+ [args, options, proc_arg]
402
+ end
403
+
404
+ # :call-seq:
405
+ # with_change_env(env_var, val, ...) { ... }
406
+ #
407
+ # When running the block, the environment will be changed as
408
+ # specified via parameters. The +env_var+ specifies the environment
409
+ # variable name, and the +val+ specifies the value, when +val+ is
410
+ # nil, the envioronment variable will be removed. Multiple pairs of
411
+ # the environment variable names and values can be specified. The
412
+ # changes to the environment are restored upon returning.
413
+ def self.with_change_env(*var_vals)
414
+ orig_var_vals = var_vals.each_slice(2).map { |var, val|
415
+ orig_val = ENV[var]
416
+ [var, orig_val]
417
+ }
418
+ var_vals.each_slice(2) { |var, val|
419
+ ENV[var] = val
420
+ }
421
+ yield
422
+ ensure
423
+ if orig_var_vals
424
+ orig_var_vals.each { |var, orig_val|
425
+ ENV[var] = orig_val
426
+ }
427
+ end
428
+ end
429
+
430
+ # :call-seq:
431
+ # chdir(dir) { ... }
432
+ #
433
+ # Same as +Dir.chdir+.
434
+ def self.chdir(dir, &b)
435
+ Dir.chdir(dir, &b)
436
+ end
437
+
438
+ def self.__spawn(args, options_in, proc_arg) # :nodoc:
439
+ if proc_arg
440
+ Process.fork {
441
+ fclose = options_in[:close] || []
442
+ fclose.each { |f| f.close }
443
+ fin = options_in[0]
444
+ fout = options_in[1]
445
+ fd_in, var_in = fin ? [0, :$stdin] : [nil, nil]
446
+ fd_out, var_out = fout ? [1, :$stdout] : [nil, nil]
447
+ with_redirect_to(fd_in, var_in, fin) {
448
+ with_redirect_to(fd_out, var_out, fout) {
449
+ begin
450
+ exit 1 unless args.first.()
451
+ rescue => e
452
+ $stderr.puts e
453
+ $stderr.puts e.backtrace
454
+ exit 1
455
+ end
456
+ }
457
+ }
458
+ }
459
+ else
460
+ options = Hash[@@redirections]
461
+ options[:close_others] = true
462
+ options.merge! options_in
463
+ Process.spawn(*args, options)
464
+ end
465
+ end
466
+
467
+ # :call-seq:
468
+ # spawn() { ... } -> pid
469
+ # spawn(function) -> pid
470
+ # spawn(command...[, options]) -> pid
471
+ #
472
+ # Run the block or the function specified via +function+ in a forked
473
+ # sub-process, or run external program specified via +command+ and
474
+ # +options+, +command+ and +options+ are same as that of
475
+ # Process.spawn. Return the +pid+.
476
+ def self.spawn(*args_in, &blk)
477
+ __spawn *__parse_args(args_in, blk)
478
+ end
479
+
480
+ # Exception class raised when an external program exits with
481
+ # non-zero status and raise_on_command_error take effect.
482
+ class CommandError < StandardError
483
+ # :call-seq:
484
+ # CommandError.new(cmdline, status)
485
+ #
486
+ # Create an instance of CommandError class, for the external
487
+ # program command line specified via +cmdline+ as an array of
488
+ # string and failed exit status specified via +status+ as
489
+ # Process::Status.
490
+ def initialize(cmdline, status)
491
+ @cmdline = cmdline
492
+ @status = status
493
+ reason = if status.exited?
494
+ "exited with #{status.exitstatus}"
495
+ else
496
+ "kill by #{status.termsig}"
497
+ end
498
+ super "#{cmdline}: #{reason}"
499
+ end
500
+
501
+ # External program command line as an array of string.
502
+ attr_reader :cmdline
503
+ # External program exit status, as Process:Status
504
+ attr_reader :status
505
+ end
506
+
507
+ def self.__check_command_status(cmd) # :nodoc:
508
+ unless $?.success?
509
+ if @@on_command_error != IGNORE
510
+ err = CommandError.new(cmd, $?)
511
+ case @@on_command_error
512
+ when WARN
513
+ $stderr.puts "Hysh: Command Error: #{err.to_s}"
514
+ when RAISE
515
+ raise err
516
+ end
517
+ end
518
+ false
519
+ else
520
+ true
521
+ end
522
+ end
523
+
524
+ def self.__run(args, options, proc_arg) #:nodoc:
525
+ if proc_arg
526
+ args.first.()
527
+ else
528
+ pid = __spawn args, options, proc_arg
529
+ Process.waitpid pid
530
+ __check_command_status(args)
531
+ end
532
+ end
533
+
534
+ # :call-seq:
535
+ # run() { ... } -> any
536
+ # run(function) -> any
537
+ # run(command...[, options]) -> true or false
538
+ #
539
+ # Run the block or the function specified via +function+ and return
540
+ # their return value. Or run external program specified via
541
+ # +command+ and +options+, +command+ and +options+ are same as that
542
+ # of Process.spawn and return whether external program the exit with
543
+ # 0. All IO redirections, environment change, current directory
544
+ # change, etc. will take effect when running the block, the function
545
+ # and the external program.
546
+ def self.run(*args_in, &blk)
547
+ __run *__parse_args(args_in, blk)
548
+ end
549
+
550
+ def self.__check_close(*ios) # :nodoc:
551
+ ios.each { |io|
552
+ if io && !io.closed?
553
+ io.close
554
+ end
555
+ }
556
+ end
557
+
558
+ def self.__popen(stdin, stdout, stderr, args, options, proc_arg) # :nodoc:
559
+ options[:close] = [] if proc_arg
560
+
561
+ stdin_in = stdin_out = nil
562
+ stdout_in = stdout_out = nil
563
+ stderr_in = stderr_out = nil
564
+ begin
565
+ if stdin
566
+ stdin_in, stdin_out = IO.pipe
567
+ options[0] = stdin_in
568
+ options[:close].push stdin_out if proc_arg
569
+ end
570
+ if stdout
571
+ stdout_in, stdout_out = IO.pipe
572
+ options[1] = stdout_out
573
+ options[:close].push stdout_in if proc_arg
574
+ end
575
+ if stderr == :stdout
576
+ raise ArgumentError.new unless stdout
577
+ options[2] = stdout_out
578
+ elsif stderr
579
+ stderr_in, stderr_out = IO.pipe
580
+ options[2] = stderr_out
581
+ end
582
+ pid = __spawn args, options, proc_arg
583
+ rescue
584
+ __check_close stdin_out, stdout_in, stderr_in
585
+ raise
586
+ ensure
587
+ __check_close stdin_in, stdout_out, stderr_out
588
+ end
589
+ values = [pid, stdin_out, stdout_in, stderr_in]
590
+ if block_given?
591
+ begin
592
+ yield *values
593
+ ensure
594
+ unless $?.pid == pid
595
+ Process.detach(pid) rescue nil
596
+ end
597
+ __check_close stdin_out, stdout_in, stderr_in
598
+ end
599
+ else
600
+ values
601
+ end
602
+ end
603
+
604
+ # :call-seq:
605
+ # popen(stdin, stdout, stderr, function) { |pid, stdin_pipe, stdout_pipe, stderr_pipe| ... }
606
+ # popen(stdin, stdout, stderr, function) -> [pid, stdin_pipe, stdout_pipe, stderr_pipe]
607
+ # popen(stdin, stdout, stderr, command...[,options]) { |pid, stdin_pipe, stdout_pipe, stderr_pipe| ... }
608
+ # popen(stdin, stdout, stderr, command...[,options]) -> [pid, stdin_pipe, stdout_pipe, stderr_pipe]
609
+ #
610
+ # Run the function specified via +function+ in a forked sub-process,
611
+ # or run external program specified via +command+ and +options+,
612
+ # +command+ and +options+ are same as that of Process.spawn.
613
+ # Redirect IO as specified via +stdin+, +stdout+, and +stderr+, any
614
+ # non-nil/false value will cause corresponding standard IO to be
615
+ # redirected to a pipe, the other end of pipe will be the block
616
+ # parameters or returned. If the value of +stderr+ argument is
617
+ # :stdout, the standard error will be redirected to standard output.
618
+ #
619
+ # If block is given, the pid and stdin, stdout and stderr pipe will
620
+ # be the parameters for the block. Popen will return the return
621
+ # value of the block. The stdin, stdout and sterr pipe will be
622
+ # closed and the process will be detached if necessary upon
623
+ # returning.
624
+ #
625
+ # If no block is given, the pid and stdin, stdout and stderr pipe
626
+ # will be returned.
627
+ def self.popen(stdin, stdout, stderr, *args_in, &blk)
628
+ args, options, proc_arg = __parse_args args_in
629
+ options[:close] = [] if proc_arg
630
+
631
+ __popen(stdin, stdout, stderr, args, options, proc_arg, &blk)
632
+ end
633
+
634
+ # :section: Glue Processes
635
+
636
+ # :call-seq:
637
+ # pipe(command_line, ...) { ... } -> any
638
+ # pipe(command_line, ...) -> any or true or false
639
+ #
640
+ # Run multiple functions or external commands specified via
641
+ # +command_line+ and the block, all functions will be run in forked
642
+ # process except the it is specified via the last argument without
643
+ # block or it is specified via the block. The stdout of the
644
+ # previous command will be connected with the stdin of the next
645
+ # command (the current process if the last argument is function
646
+ # without block or the block), that is, a pipeline is constructed to
647
+ # run the commands. If the last argument specifies a function
648
+ # without block or there is a block, return the return value of the
649
+ # function or the block. Otherwise, return the exit success status
650
+ # of the last external program.
651
+ #
652
+ # +command_line+ could be
653
+ # [command, ..., options] # command with argument and options in an array
654
+ # [command, ...] # command with/without arguments in an array
655
+ # command # command without argument
656
+ # [function] # function in an array
657
+ # function # function
658
+ def self.pipe(*cmds, &blk)
659
+ if block_given?
660
+ cmds.push [blk]
661
+ end
662
+ if cmds.empty?
663
+ raise ArgumentError.new('No argument or block!')
664
+ elsif cmds.size == 1
665
+ __run *__parse_args(cmds.first)
666
+ else
667
+ begin
668
+ pin = pout = prev_pout = last_pin = nil
669
+
670
+ last_args, last_options, last_proc_arg = __parse_args cmds.last
671
+ pin, pout = IO.pipe
672
+ if last_proc_arg
673
+ closefs = [pin]
674
+ last_pin = pin
675
+ else
676
+ last_options[0] = pin
677
+ last_pid = __spawn last_args, last_options, false
678
+ closefs = []
679
+ pin.close
680
+ end
681
+ pin = nil
682
+
683
+ cmds[1..cmds.size-2].reverse.each { |cmd|
684
+ args, options, proc_arg = __parse_args cmd
685
+ prev_pout = pout
686
+ pout = nil
687
+ pin, pout = IO.pipe
688
+ options[0] = pin
689
+ options[1] = prev_pout
690
+ if proc_arg
691
+ options[:close] = closefs + [pout]
692
+ end
693
+ pid = __spawn args, options, proc_arg
694
+ Process.detach pid
695
+ pin.close
696
+ pin = nil
697
+ prev_pout.close
698
+ prev_pout = nil
699
+ }
700
+
701
+ args, options, proc_arg = __parse_args cmds.first
702
+ options[1] = pout
703
+ if proc_arg
704
+ options[:close] = closefs
705
+ end
706
+ pid = __spawn args, options, proc_arg
707
+ pout.close
708
+ pout = nil
709
+ Process.detach pid
710
+
711
+ if last_proc_arg
712
+ ret = nil
713
+ with_redirect_stdin_to(last_pin) {
714
+ ret = __run last_args, last_options, true
715
+ }
716
+ last_pin.close
717
+ last_pin = nil
718
+ ret
719
+ else
720
+ Process.waitpid last_pid
721
+ __check_command_status cmds
722
+ end
723
+ ensure
724
+ __check_close pin, pout, prev_pout, last_pin
725
+ end
726
+ end
727
+ end
728
+
729
+ # :call-seq:
730
+ # run_seq(command_line, ...) { ... } -> any
731
+ # run_seq(command_line, ...) -> any or true or false
732
+ #
733
+ # Run the functions and the external programs specified via
734
+ # +command_line+, and the block if given, from left to right.
735
+ # +command_line+ is same as that of +pipe+. Return the return value
736
+ # or exit success status of the last function or external command.
737
+ def self.run_seq(*cmds, &blk)
738
+ if block_given?
739
+ cmds.push blk
740
+ end
741
+ ret = true
742
+ cmds.each { |cmd|
743
+ ret = run(cmd)
744
+ }
745
+ ret
746
+ end
747
+
748
+ # :call-seq:
749
+ # run_or(command_line, ...) { ... } -> any
750
+ # run_or(command_line, ...) -> any or true or false
751
+ #
752
+ # Run the functions and the external programs specified via
753
+ # +command_line+, and the block if given, from left to right.
754
+ # +command_line+ is same as that of +pipe+. If any function or
755
+ # block returns non-nil/false, or any external program exits
756
+ # successfully, stop running the remaining function, or external
757
+ # program and return the value. If all failed, false or nil will be
758
+ # returned. If no function, external program, or block is given,
759
+ # return false.
760
+ def self.run_or(*cmds, &blk)
761
+ if block_given?
762
+ cmds.push blk
763
+ end
764
+ return false if cmds.empty?
765
+
766
+ *head_cmds, last_cmd = cmds
767
+ ignore_on_command_error {
768
+ head_cmds.each { |cmd|
769
+ if ret = run(cmd)
770
+ return ret
771
+ end
772
+ }
773
+ }
774
+ run(last_cmd)
775
+ end
776
+
777
+ # :call-seq:
778
+ # run_and(command_line, ...) { ... } -> any
779
+ # run_and(command_line, ...) -> any or true or false
780
+ #
781
+ # Run the functions and the external programs specified via
782
+ # +command_line+, and the block if given, from left to right.
783
+ # +command_line+ is same as that of +pipe+. If any function or
784
+ # block returns nil or false, or any external program exits failed,
785
+ # stop running the remaining function, or external program and
786
+ # return the value. If all succeed, return the return value of the
787
+ # last function, the block or the exit success status of the
788
+ # external program. If no function, external program, or block is
789
+ # provided, return true.
790
+ def self.run_and(*cmds, &blk)
791
+ if block_given?
792
+ cmds.push blk
793
+ end
794
+ return true if cmds.empty?
795
+
796
+ *head_cmds, last_cmd = cmds
797
+ ignore_on_command_error {
798
+ head_cmds.each { |cmd|
799
+ unless ret = run(cmd)
800
+ return ret
801
+ end
802
+ }
803
+ }
804
+ run(last_cmd)
805
+ end
806
+
807
+ # :section: Filter Helpers
808
+
809
+ # :call-seq:
810
+ # filter_line() { |line| ... } -> true
811
+ #
812
+ # Feed each line from $stdin to the block, if non-nil/false
813
+ # returned, write the return value to the $stdout.
814
+ def self.filter_line
815
+ $stdin.each_line { |line|
816
+ if ret_line = yield(line)
817
+ $stdout.write ret_line
818
+ end
819
+ }
820
+ true
821
+ end
822
+
823
+ # :call-seq:
824
+ # filter_char() { |char| ... } -> true
825
+ #
826
+ # Feed each character from $stdin to the block, if non-nil/false
827
+ # returned, write the return value to the $stdout.
828
+ def self.filter_char
829
+ $stdin.each_char { |ch|
830
+ if ret_ch = yield(ch)
831
+ $stdout.write ret_ch
832
+ end
833
+ }
834
+ true
835
+ end
836
+ end
@@ -0,0 +1,39 @@
1
+ # -*- ruby-indent-level: 2; -*-
2
+
3
+ require_relative "../lib/hysh.rb"
4
+
5
+ describe Hysh do
6
+ describe ".run" do
7
+ it "run command and return whether exit with 0" do
8
+ expect(Hysh.run('true')).to eql(true)
9
+ expect(Hysh.run('false')).to eql(false)
10
+ end
11
+
12
+ it "call ruby function" do
13
+ expect(Hysh.run { true }).to eql(true)
14
+ expect(Hysh.run { false }).to eql(false)
15
+ end
16
+ end
17
+
18
+ describe ".out_s" do
19
+ it "run command and return its output" do
20
+ expect(Hysh.out_s("echo", "-n", "abc")).to eql(["abc", true])
21
+ end
22
+
23
+ it "run ruby function in process and return its output" do
24
+ expect(Hysh.out_s ->{ $stdout.write "abc" }).to eql(["abc", 3])
25
+ end
26
+ end
27
+
28
+ describe ".io_s" do
29
+ it "run command, given input and return its output" do
30
+ expect(Hysh.io_s("abc", "tr", "ab", "AB")).to eql(["ABc", true])
31
+ end
32
+ end
33
+
34
+ describe ".pipe" do
35
+ it "run commands in pipe line" do
36
+ expect(Hysh.out_s ->{ Hysh.pipe(["echo", "-n", "abc"], ["tr", "ab", "AB"]) }).to eql(["ABc", true])
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,33 @@
1
+ # -*- ruby-indent-level: 2; -*-
2
+
3
+ require_relative "../lib/hysh"
4
+
5
+ def dpkg_installed1(package_names = nil)
6
+ Hysh.out_lines ->{
7
+ Hysh.pipe ['dpkg', '-l'],
8
+ if package_names
9
+ ['egrep', "(#{package_names.join '|'})"]
10
+ else
11
+ ['cat']
12
+ end
13
+ }
14
+ end
15
+
16
+ def dpkg_installed2(package_names = nil)
17
+ Hysh.out_lines ->{
18
+ Hysh.pipe ['dpkg', '-l'] {
19
+ proc_line = if package_names
20
+ ->l{
21
+ if package_names.any? { |pkg|
22
+ l.index pkg
23
+ }
24
+ l
25
+ end
26
+ }
27
+ else
28
+ ->l{ l }
29
+ end
30
+ Hysh.filter_line &proc_line
31
+ }
32
+ }
33
+ end
metadata ADDED
@@ -0,0 +1,50 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: hysh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Huang, Ying
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-09-12 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: IO redirection and command glue in Ruby
14
+ email: huang.ying.caritas@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - Gemfile
20
+ - README.rdoc
21
+ - Rakefile
22
+ - lib/hysh.rb
23
+ - spec/hysh_spec.rb
24
+ - test/dpkg_test.rb
25
+ homepage: http://github.com/hying-caritas/ruby-hysh
26
+ licenses:
27
+ - LGPL
28
+ metadata: {}
29
+ post_install_message:
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubyforge_project:
45
+ rubygems_version: 2.2.2
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: Huang Ying's SHell in Ruby
49
+ test_files: []
50
+ has_rdoc: