easysh 0.1.0

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.
Files changed (3) hide show
  1. data/lib/easysh.rb +185 -0
  2. data/test/test_easysh.rb +105 -0
  3. metadata +48 -0
data/lib/easysh.rb ADDED
@@ -0,0 +1,185 @@
1
+ # EasySH examples:
2
+ #
3
+ # sh = EasySH.new
4
+ #
5
+ # # basic usage
6
+ # puts sh.ls
7
+ # puts sh['/bin/ls']
8
+ #
9
+ # # command parameters
10
+ # puts sh.ls['/bin']._l
11
+ # puts sh.ls._l '/bin'
12
+ # puts sh.ls('/bin', :l, :color => :always)
13
+ # puts sh['/bin/ls', '-l', :color => :always]
14
+ #
15
+ # # enumerable
16
+ # sh.ls.max
17
+ # sh.ls.sort
18
+ # sh.ls.chars.to_a.sample(5)
19
+ # sh.cat('/dev/urandom').bytes.first(10)
20
+ # sh.ls._l.map{|l| l.split}
21
+ # sh.ps._e._o('euser,comm').map(&:split).group_by(&:first)
22
+ #
23
+ # # chaining commands
24
+ # sudo = sh.sudo
25
+ # tail = sh.tail
26
+ # sudo[tail._f '/var/log/everything.log'].lines { |l| puts l.upcase }
27
+ # sudo.tail._f '/var/log/everything.log' do |l| puts l.upcase end
28
+ #
29
+ # lab = sh.ssh['lab'] # or, sh.ssh.lab
30
+ # puts lab.ls._l '/bin', color: :always
31
+ #
32
+ # # redirects
33
+ # puts sh.echo('hello') > '/tmp/test'
34
+ # puts sh.echo 'hello', 1 => '/tmp/stdout', 2 => '/tmp/stderr'
35
+ # puts sh.cat < '/tmp/test'
36
+ # puts sh.cat 0 => '/tmp/fffff'
37
+ #
38
+ # # pipes
39
+ # puts sh.man('ls') | sh.tail(n: 30) | sh.head(:n, 4)
40
+ #
41
+ # grep = sh['grep']
42
+ # filter = grep['problem'] | grep._v['bugs']
43
+ # puts sh.man.ls | filter
44
+ #
45
+ # kat = sh.cat
46
+ # puts kat['/tmp/foo'] | (kat | kat | kat.|(kat) | (kat | kat) | (kat | kat))
47
+ #
48
+ # # exit status
49
+ # p = sh.which('bash')
50
+ # puts p
51
+ # p.exitstatus # => #<Process::Status: pid 5931 exit 0>
52
+ # p = sh.which.nonexists
53
+ # puts p
54
+ # p.exitstatus # => #<Process::Status: pid 6156 exit 1>
55
+ #
56
+ #
57
+ # # instant mode
58
+ # # tired with 'puts' and 'to_s' in REPL? just set instant = true
59
+ # # [2] pry(main)> sh.instant = false; sh.uptime
60
+ # # => #<EasySH: uptime>
61
+ # # [3] pry(main)> sh.instant = true; sh.uptime
62
+ # # => 22:14:23 up 1 day, 4:02, 12 users, load average: 0.69, 0.65, 0.67
63
+ # # [4] pry(main)> sh = EasySH.instant; sh.uname
64
+ # # => Linux
65
+ #
66
+ #
67
+ class EasySH < Struct.new(:cmd, :opt, :chain, :instant)
68
+ include Enumerable
69
+
70
+ attr_reader :exitstatus
71
+
72
+ def method_missing name, *args, &block # :no-doc:
73
+ begin
74
+ return super(name, *args, &block)
75
+ rescue NoMethodError, ArgumentError => ex
76
+ # continue
77
+ end
78
+
79
+ r = if name.is_a? EasySH
80
+ self.class.new [*cmd, *name.cmd], Hash[*opt, *name.opt], chain, instant
81
+ else
82
+ args = [name.to_s.gsub(/^_+/) {|s| '-' * s.size}, *args]
83
+ *args, opt = *args if args.last.is_a?(Hash) && args.last.keys.find{|k| k.is_a? Integer}
84
+ args = args.map do |a|
85
+ case a
86
+ when Symbol
87
+ "-#{a.length > 1 ? '-' : ''}#{a}"
88
+ when Hash
89
+ a.map { |k,v| k.length > 1 ? "--#{k}=#{v}" : "-#{k} #{v}" }
90
+ else
91
+ a.to_s
92
+ end
93
+ end.flatten
94
+ self.class.new [*cmd, *args], Hash[[*self.opt, *opt]], chain, instant
95
+ end
96
+ block ? r.each(&block) : r
97
+ end
98
+
99
+ def to_a; each.to_a; end
100
+ def to_s(n = "\n"); cmd ? to_a.join(n) : ''; end
101
+
102
+ def inspect
103
+ instant ? to_s : "#<#{self.class}: #{([*chain, [cmd]]).map(&:first).map {|c| c && c.join(' ')}.compact.join(' | ')}>"
104
+ end
105
+
106
+ def |(sh, &block)
107
+ raise TypeError.new("EasySH expected, got #{sh.inspect}") unless sh.is_a? EasySH
108
+ self.class.new sh.cmd, sh.opt, [*chain, cmd && [cmd, opt || {}], *sh.chain].compact, instant
109
+ end
110
+
111
+ def < path; self.opt[0] = path; self; end
112
+ def > path; self.opt[1] = path; self; end
113
+
114
+ def to_io
115
+ return unless cmd
116
+
117
+ cur_opt = opt.clone
118
+ cur_chain = (chain || []) + [[cmd, cur_opt]]
119
+ pipes = cur_chain.map { IO.pipe }
120
+ n = pipes.size
121
+ cur_opt[0] = pipes[n-1][0] if n > 1
122
+ lpid, lopt = nil
123
+ pids = []
124
+
125
+ begin
126
+ cur_chain.reverse.each_with_index do |cmd_opt, i|
127
+ i = n - 1 - i
128
+ c, o = *cmd_opt
129
+ o = o.clone
130
+ o[1] = nil if i < n - 1
131
+ o[1] ||= pipes[i][1]
132
+ o[0] = pipes[i-1][0] if i > 0
133
+ pid = spawn(*c, o)
134
+ pids << pid
135
+ lopt, lpid = o, pid if i == n - 1
136
+ end
137
+
138
+ if lopt[1] == pipes[n-1][1]
139
+ rfd = pipes[n-1][0]
140
+ (pipes.flatten-[rfd]).each { |io| io.close unless io.closed? }
141
+ yield rfd
142
+ end
143
+ ensure
144
+ pipes.flatten.each { |io| io.close unless io.closed? }
145
+ @exitstatus = Process.wait2(lpid)[-1] rescue nil
146
+ ['TERM', 'KILL'].each { |sig| Process.kill sig, *pids rescue nil }
147
+ Process.waitall rescue nil
148
+ end
149
+ end
150
+
151
+ def each_line
152
+ return to_enum(:each_line) unless block_given?
153
+ to_io { |i| while l = i.gets; yield l.chomp; end }
154
+ end
155
+
156
+ def each_char
157
+ return to_enum(:each_char) unless block_given?
158
+ to_io { |i| while c = i.getc; yield c; end }
159
+ end
160
+
161
+ def each_byte
162
+ return to_enum(:each_byte) unless block_given?
163
+ to_io { |i| while b = i.getbyte; yield b; end }
164
+ end
165
+
166
+ alias :call :method_missing
167
+ alias :[] :method_missing
168
+ alias :to_ary :to_a
169
+ alias :lines :each_line
170
+ alias :each :each_line
171
+ alias :lines :each_line
172
+ alias :chars :each_char
173
+ alias :bytes :each_byte
174
+ alias :read :to_s
175
+ alias :! :to_s
176
+
177
+ def pretty_print(q)
178
+ q.text self.inspect
179
+ end
180
+
181
+ def self.instant
182
+ new(nil, {}, [], true)
183
+ end
184
+ end
185
+
@@ -0,0 +1,105 @@
1
+ require 'test/unit'
2
+ require 'easysh'
3
+ require 'fileutils'
4
+ require 'tmpdir'
5
+
6
+ class EasySHTest < Test::Unit::TestCase
7
+
8
+ def sh; @sh ||= EasySH.new; end
9
+
10
+ def with_tmpfile(content = [*0..5].join("\n"))
11
+ tmppath = File.join(Dir.tmpdir, 'test.txt')
12
+ begin
13
+ File.open(tmppath, 'w') { |f| f.write content }
14
+ yield tmppath, content
15
+ ensure
16
+ FileUtils.rm_f tmppath
17
+ end
18
+ end
19
+
20
+ def fd_count
21
+ Dir['/proc/self/fd/*'].count
22
+ end
23
+
24
+ def assert_fd_count_equal
25
+ count = fd_count
26
+ yield
27
+ assert_equal fd_count, count
28
+ end
29
+
30
+ def test_execute
31
+ with_tmpfile do |tmppath, content|
32
+ assert_equal sh.cat(tmppath).to_s, content
33
+ assert_equal sh.cat[tmppath].to_s, content
34
+ assert_equal sh['cat', tmppath].to_s, content
35
+ kat = sh.cat[tmppath]
36
+ assert_equal kat.to_s, content
37
+ end
38
+ end
39
+
40
+ def test_enumerator
41
+ with_tmpfile [*10..25].join("\n") do |tmppath, content|
42
+ kat = sh.cat
43
+ kat_t = kat[tmppath]
44
+ assert_equal kat[tmppath].min, '10'
45
+ assert_equal kat_t.max, '25'
46
+
47
+ en = kat_t.each
48
+ assert_equal en.min, '10'
49
+ assert_equal en.max, '25'
50
+
51
+ assert_equal kat_t.chars.min, "\n"
52
+ assert_equal kat_t.bytes.min, "\n".ord
53
+ end
54
+ end
55
+
56
+ def test_options
57
+ with_tmpfile do |tmppath|
58
+ assert_include sh.ls._l(tmppath).to_s, '-'
59
+ assert_equal sh.ls._l(tmppath).read, sh.ls(:l)[tmppath].to_s
60
+ assert_equal sh.ls._l(tmppath).to_s, sh.ls['-l', tmppath].read
61
+ end
62
+ end
63
+
64
+ def test_pipes
65
+ kat = sh.cat
66
+
67
+ with_tmpfile do |tmppath, content|
68
+ assert_fd_count_equal do
69
+ assert_equal kat[tmppath].to_s, content
70
+ assert_equal (kat[tmppath] | kat | kat | kat | kat | kat | kat | kat | kat | kat).to_s, content
71
+ assert_equal (kat[tmppath] | kat | (kat | (kat | kat | kat) | kat | kat) | kat | kat).to_s, content
72
+ end
73
+ end
74
+ end
75
+
76
+ def test_broken_pipes
77
+ kat = sh.cat
78
+ assert_fd_count_equal do
79
+ assert_equal (sh.echo('hello') | kat | sh.false | kat | kat).read.empty?, true
80
+ end
81
+ assert_raise(TypeError) { sh.echo('hello') | 'abc' }
82
+ end
83
+
84
+ def test_instant
85
+ kat = sh.cat
86
+ kat.instant = true
87
+
88
+ with_tmpfile do |tmppath, content|
89
+ assert_fd_count_equal do
90
+ assert_equal kat[tmppath].inspect, content
91
+ end
92
+ end
93
+ end
94
+
95
+ def test_redirects
96
+ kat = sh.cat
97
+ with_tmpfile do |tmppath, content|
98
+ assert_fd_count_equal do
99
+ assert_equal ((kat < tmppath) | kat).to_s, content
100
+ ((sh.echo('hello') | kat) > tmppath).!
101
+ assert_equal File.read(tmppath).chomp, 'hello'
102
+ end
103
+ end
104
+ end
105
+ end
metadata ADDED
@@ -0,0 +1,48 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: easysh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Wu Jun
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-17 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: Synatactic sugars about shell executables, redirects and pipes
15
+ email: quark@zju.edu.cn
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - lib/easysh.rb
21
+ - test/test_easysh.rb
22
+ homepage: https://github.com/quark-zju/easyshell
23
+ licenses: []
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 1.8.23
43
+ signing_key:
44
+ specification_version: 3
45
+ summary: Make shell task easier for ruby script.
46
+ test_files:
47
+ - test/test_easysh.rb
48
+ has_rdoc: