hysh 0.0.1

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